Compare commits

...

45 Commits

Author SHA1 Message Date
github-actions[bot] 57f69c6615 Publish New Versions (v2) (#1966)
Co-authored-by: FabianLars <FabianLars@users.noreply.github.com>
2024-11-05 17:32:55 +01:00
Fabian-Lars cfb3ec0e21 fix(window-state): Ignore is_maximized state in resize events on macos (#2007) 2024-11-05 17:01:35 +01:00
renovate[bot] b8bf4ad360 chore(deps): update unocss monorepo to ^0.64.0 (v2) (#2006)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-05 13:23:46 +01:00
Artaza Sameen 87cc58527d feat(upload): Add transfer_speed for downloading and uploading files (#1797)
Co-authored-by: Victor Aremu <me@victorare.mu>
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2024-11-04 22:41:48 +01:00
renovate[bot] e0d2e2c53f chore(deps): update dependency typescript-eslint to v8.13.0 (#2004)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 19:36:33 +01:00
renovate[bot] 9a7092ca44 fix(deps): update rust crate notify to v7 (v2) (#1999)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FabianLars <fabianlars@fabianlars.de>
2024-11-04 15:56:31 +01:00
renovate[bot] 606fa08dae fix(deps): update rust crate fern to 0.7 (v2) (#1997)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Fabian-Lars <fabianlars@fabianlars.de>
2024-11-04 15:00:01 +01:00
renovate[bot] 62f1e40682 chore(deps): update eslint monorepo to v9.14.0 (v2) (#1996)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 13:23:13 +01:00
renovate[bot] c8e5614063 chore(deps): update dependency svelte to v5 (v2) (#1956)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FabianLars <fabianlars@fabianlars.de>
2024-11-04 13:16:36 +01:00
Fabian-Lars 03c255ae20 chore(readme): Add new plugins and update support table (#1993) 2024-11-04 13:57:38 +02:00
renovate[bot] a706748dd3 fix(deps): update tauri monorepo (v2) (#1994)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 12:29:34 +01:00
renovate[bot] 7c1046d239 chore(deps): update dependency rollup to v4.24.4 (v2) (#1844)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 12:28:44 +01:00
renovate[bot] a90f36b07e chore(deps): update dependency typescript-eslint to v8.12.2 (v2) (#1995)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 12:25:26 +01:00
renovate[bot] e5249cff0b chore(deps): update dependency tslib to v2.8.1 (v2) (#1943)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 12:06:26 +01:00
Rika d57df4debe fix(clipboard-manager): read_image wrongly set the image rgba data with binary PNG data. (#1986)
* fix(clipboard-manager): `read_image` wrongly set the image rgba data with binary PNG data.

* remove depencency of `image` crate; add patch file
2024-10-30 20:29:14 +03:00
Amr Bashir 1f649c7f1f chore: print a warning in development for forbidden headers (#1981)
* chore: print a warning in development for forbidden headers

closes #1979

* Update plugins/http/src/commands.rs
2024-10-30 15:26:46 +03:00
YoggieS 0ca4cc914c docs(sql): docs of frontend Database select method (#1963) 2024-10-30 10:53:04 +01:00
Amr Bashir 9dcad78f44 docs(fs): fix default permission documentation (#1980) 2024-10-30 10:50:00 +01:00
阿良仔 4341d7f500 fix(sql): replace Mutex with RwLock to enable concurrent SQL execution (#1972) 2024-10-26 13:19:59 +02:00
Amr Bashir 77149dc432 fix(fs): fix writeFile converting UTF-8 characters in path into replacement character (#1965) 2024-10-22 08:17:37 +03:00
github-actions[bot] 525abc4be5 Publish New Versions (v2) (#1961)
Co-authored-by: amrbashir <amrbashir@users.noreply.github.com>
2024-10-21 15:13:27 +03:00
bWanShiTong 14cee64c82 fix(fs): fix compilation on targets with pointer width 16 and 32 (#1958) 2024-10-21 15:07:55 +03:00
github-actions[bot] 3fd283121f publish new versions (#1909)
Co-authored-by: lucasfernog <lucasfernog@users.noreply.github.com>
2024-10-20 08:54:26 -03:00
Amr Bashir ae8024565f perf(fs): improve FileHandle.read performance (#1950)
* perf(fs): improve `FileHandle.read` performance

* handle different target pointer width

* improve `writeTextFile` performance

* revert packageManager field

* change file

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2024-10-20 08:49:01 -03:00
Tony 2302c2db1c fix(dialog): ask and confirm not using system button texts (#1910)
* Fix `ask`'s button texts being ok and cancel

* Update change file
2024-10-20 08:48:45 -03:00
renovate[bot] 44c50c1275 chore(deps): update rust crate tauri to 2.0.4 (#1952)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-20 17:28:39 +08:00
renovate[bot] 415bf2abc3 chore(deps): update eslint monorepo to v9.13.0 (#1951)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-20 16:54:34 +08:00
renovate[bot] 36207a93f3 chore(deps): update dependency @tauri-apps/cli to v2.0.3 (#1925)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-19 11:14:14 +08:00
renovate[bot] 854754e10b chore(deps): update dependency typescript-eslint to v8.10.0 (#1949)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-19 11:13:06 +08:00
renovate[bot] 1c2f137a8a chore(deps): lock file maintenance (#1929)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-19 11:12:38 +08:00
Tony 8c67d44aef refactor(store)!: more reworks (#1860)
* refactor(store): more reworks

* Enable auto save by default

* Store to resource table by default

* Remove share store

* Clean up

* Add close store

* Add store function

* Add lazy store

* Add init to lazy store

* refresh cache in example

* Add get-or-create-store

* Revert "Add get-or-create-store"

This reverts commit 7ffd769240.

* try get first

* Docs

* Use absolute path for store

* more docs

* Allow js to use pre-stored (de)serialize functions

* Fix js get and close store

* Show case how to use pretty json

* Update readme

* Use store instead of `store_builder` in example

* Build

* Fix example

* More docs for StoreBuilder::build

* Add default (de)serialize fn

* Use pretty json by default

* Use `undefined` for empty value in get

* Change files

* Differentiate json null from no value for events

* Add create or existing

* Build

* Rename inner store's inset method to set

* Update readme

* Apply suggestions from code review

* Use close instead

* Update breaking changes

* Return result in resolve_store_path

* Change to close_resource and take &self

* Clean up

* Apply suggestions from code review

Co-authored-by: Amr Bashir <amr.bashir2015@gmail.com>

* Remove unused pub(crate)

* Update change file

* Expose resolve_store_path

* Remove with_store

* Remove StoreInner from pub and expose is_empty

* Fix wrong jsdoc param

* Update readme

* rename createOrExistingStore to createOrLoad

* make api consistent with the JS implementation, add examples

* fmt

* reintroduce "get existing store" behavior for create_or_load

* rename createOrLoad to newOrExisting

* keep store lock throughout whole new_or_existing_inner

* Remove load

* Missed if load

* Don't make StoreState public

* Remove R: Runtime from Builder

* rename newOrExisting to load, load to reload

* update docs

* rename missing reload fn

* rename builder fn to build()

* fix default permission

* Fix description and create_new logic

* Clippy

* Update docs

* Update docs

* remove create_store command

* remove close_store command since we extend from Resource

* Revert "remove close_store command since we extend from Resource"

This reverts commit 4a29fc8990.

* Reapply "remove close_store command since we extend from Resource"

This reverts commit 70a1830e7d.

---------

Co-authored-by: Amr Bashir <amr.bashir2015@gmail.com>
Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2024-10-17 19:14:41 +08:00
Niko Korvenlaita cfd48b3b2e feat: allow http calls without origin header (#1941) 2024-10-16 15:56:36 +03:00
renovate[bot] 558e14bb4c chore(deps): update dependency typescript-eslint to v8.9.0 (#1937)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 09:52:18 +08:00
凊弦凝绝 aef5dd6157 fix(sql): don't panic on missing migration when preload is enabled (#1932)
Co-authored-by: Fabian-Lars <fabianlars@fabianlars.de>
2024-10-14 20:32:49 +02:00
Fabian-Lars 74ee7edc43 ci: Run workflows on ubuntu 22.04 (#1936) 2024-10-14 18:40:56 +02:00
Fabian-Lars 7e1c17a635 ci(renovate): Remove tauri pre-release workaround 2024-10-09 14:44:38 +02:00
Lucas Fernandes Nogueira 3c1f3874f4 fix(positioner): handleIconState missing permission and wrong impl (#1911) 2024-10-09 09:37:16 -03:00
Amr Bashir 51ddf6a715 fix(shell): use async command for open JS API (#1881)
closes tauri-apps/tauri#11212
2024-10-09 08:47:25 -03:00
renovate[bot] 62082b7086 chore(deps): lock file maintenance (#1895)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-09 08:46:38 -03:00
nashaofu aee14ed426 fix(dialog): Fix the issue of Android saving files with a .txt extension (#1892)
* fix: Fix the issue of Android saving files with a .txt extension

* fix(dialog): pull intent type from extensions filters

* fix covector

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2024-10-09 08:45:05 -03:00
Amr Bashir 9b2840db94 fix(http): retain headers order (#1884)
* fix(http): retain headers order

closes #1882

* simplify if

* actually set headers

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2024-10-09 08:07:46 -03:00
renovate[bot] 60064fce52 chore(deps): update dependency typescript to v5.6.3 (#1905)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-09 09:28:06 +08:00
renovate[bot] 0058583583 fix(deps): update tauri monorepo to v2.0.2 (#1874)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-08 15:29:28 +08:00
renovate[bot] fb676626f1 chore(deps): update dependency typescript-eslint to v8.8.1 (#1899)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-08 10:01:07 +08:00
renovate[bot] f445c704a1 chore(deps): update eslint monorepo to v9.12.0 (#1887)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 07:29:30 +08:00
107 changed files with 2856 additions and 2166 deletions
+1 -1
View File
@@ -126,7 +126,7 @@ jobs:
clippy:
needs: changes
if: ${{ needs.changes.outputs.packages != '[]' && needs.changes.outputs.packages != '' }}
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
+1 -1
View File
@@ -168,7 +168,7 @@ jobs:
}
- {
target: x86_64-unknown-linux-gnu,
os: ubuntu-latest,
os: ubuntu-22.04,
runner: 'cargo',
command: 'test'
}
Generated
+308 -642
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"] }
log = "0.4"
tauri = { version = "2.0.1", default-features = false }
tauri = { version = "2.0.4", default-features = false }
tauri-build = "2.0.1"
tauri-plugin = "2.0.1"
tauri-utils = "2.0.1"
+16 -8
View File
@@ -1,16 +1,22 @@
# Official Tauri Plugins
This repo and all plugins require a Rust version of at least **1.77.2**
## Plugins Found Here
| | | Win | Mac | Lin | iOS | And |
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | --- | --- | --- | --- |
| [autostart](plugins/autostart) | Automatically launch your app at system startup. | ✅ | ✅ | ✅ | ? | ? |
| [autostart](plugins/autostart) | Automatically launch your app at system startup. | ✅ | ✅ | ✅ | | |
| [barcode-scanner](plugins/barcode-scanner) | Allows your mobile application to use the camera to scan QR codes, EAN-13 and other kinds of barcodes. | ? | ? | ? | ✅ | ✅ |
| [biometric](plugins/biometric) | Prompt the user for biometric authentication on Android and iOS. | ? | ? | ? | ✅ | ✅ |
| [cli](plugins/cli) | Parse arguments from your Command Line Interface | ✅ | ✅ | ✅ | ? | ? |
| [cli](plugins/cli) | Parse arguments from your Command Line Interface | ✅ | ✅ | ✅ | | |
| [clipboard-manager](plugins/clipboard-manager) | Read and write to the system clipboard. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [deep-link](plugins/deep-link) | Set your Tauri application as the default handler for an URL. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [dialog](plugins/dialog) | Native system dialogs for opening and saving files along with message dialogs. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [fs](plugins/fs) | Access the file system. | ✅ | ✅ | ✅ | ? | ? |
| [geolocation](plugins/geolocation) | Get and track current device position. | ? | ? | ? | ✅ | ✅ |
| [global-shortcut](plugins/global-shortcut) | Register global shortcuts. | ✅ | ✅ | ✅ | ? | ? |
| [haptics](plugins/haptics) | Haptic feedback and vibrations. | ? | ? | ? | ✅ | ✅ |
| [http](plugins/http) | Access the HTTP client written in Rust. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [localhost](plugins/localhost) | Use a localhost server in production apps. | ✅ | ✅ | ✅ | ? | ? |
| [log](plugins/log) | Configurable logging. | ✅ | ✅ | ✅ | ✅ | ✅ |
@@ -18,19 +24,21 @@
| [notification](plugins/notification) | Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [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. | ✅ | ✅ | ✅ | ? | ? |
| [positioner](plugins/positioner) | Move windows to common locations. | ✅ | ✅ | ✅ | | |
| [process](plugins/process) | This plugin provides APIs to access the current process. To spawn child processes, see the [`shell`](https://github.com/tauri-apps/tauri-plugin-shell) plugin. | ✅ | ✅ | ✅ | ? | ? |
| [shell](plugins/shell) | Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application. | ✅ | ✅ | ✅ | ? | ? |
| [single-instance](plugins/single-instance) | Ensure a single instance of your tauri app is running. | ✅ | ? | ✅ | ? | ? |
| [sql](plugins/sql) | Interface with SQL databases. | ✅ | ✅ | ✅ | ? | ? |
| [single-instance](plugins/single-instance) | Ensure a single instance of your tauri app is running. | ✅ | | ✅ | | |
| [sql](plugins/sql) | Interface with SQL databases. | ✅ | ✅ | ✅ | ? | |
| [store](plugins/store) | Persistent key value storage. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [stronghold](plugins/stronghold) | Encrypted, secure database. | ✅ | ✅ | ✅ | ? | ? |
| [updater](plugins/updater) | In-app updates for Tauri applications. | ✅ | ✅ | ✅ | ? | ? |
| [updater](plugins/updater) | In-app updates for Tauri applications. | ✅ | ✅ | ✅ | | |
| [upload](plugins/upload) | Tauri plugin for file uploads through HTTP. | ✅ | ✅ | ✅ | ? | ? |
| [websocket](plugins/websocket) | Open a WebSocket connection using a Rust client in JS. | ✅ | ✅ | ✅ | ? | ? |
| [window-state](plugins/window-state) | Persist window sizes and positions. | ✅ | ✅ | ✅ | ? | ? |
| [window-state](plugins/window-state) | Persist window sizes and positions. | ✅ | ✅ | ✅ | | |
_This repo and all plugins require a Rust version of at least **1.77.2**_
- ✅: (Partially) Supported
- ❌: Not supported
- `?` : Unknown/Untested or Planned
## Contributing
+16
View File
@@ -1,5 +1,21 @@
# Changelog
## \[2.0.2]
### Dependencies
- Upgraded to `fs-js@2.0.2`
## \[2.0.1]
### Dependencies
- Upgraded to `dialog-js@2.0.1`
- Upgraded to `fs-js@2.0.1`
- Upgraded to `http-js@2.0.1`
- Upgraded to `shell-js@2.0.1`
- Upgraded to `store-js@2.1.0`
## \[2.0.0]
- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release.
+12 -12
View File
@@ -1,7 +1,7 @@
{
"name": "svelte-app",
"private": true,
"version": "2.0.0",
"version": "2.0.2",
"type": "module",
"scripts": {
"dev": "vite --clearScreen false",
@@ -9,34 +9,34 @@
"serve": "vite preview"
},
"dependencies": {
"@tauri-apps/api": "2.0.1",
"@tauri-apps/api": "2.0.3",
"@tauri-apps/plugin-barcode-scanner": "2.0.0",
"@tauri-apps/plugin-biometric": "2.0.0",
"@tauri-apps/plugin-cli": "2.0.0",
"@tauri-apps/plugin-clipboard-manager": "2.0.0",
"@tauri-apps/plugin-dialog": "2.0.0",
"@tauri-apps/plugin-fs": "2.0.0",
"@tauri-apps/plugin-dialog": "2.0.1",
"@tauri-apps/plugin-fs": "2.0.2",
"@tauri-apps/plugin-geolocation": "2.0.0",
"@tauri-apps/plugin-global-shortcut": "2.0.0",
"@tauri-apps/plugin-haptics": "2.0.0",
"@tauri-apps/plugin-http": "2.0.0",
"@tauri-apps/plugin-http": "2.0.1",
"@tauri-apps/plugin-nfc": "2.0.0",
"@tauri-apps/plugin-notification": "2.0.0",
"@tauri-apps/plugin-os": "2.0.0",
"@tauri-apps/plugin-process": "2.0.0",
"@tauri-apps/plugin-shell": "2.0.0",
"@tauri-apps/plugin-store": "2.0.0",
"@tauri-apps/plugin-shell": "2.0.1",
"@tauri-apps/plugin-store": "2.1.0",
"@tauri-apps/plugin-updater": "2.0.0",
"@zerodevx/svelte-json-view": "1.0.11"
},
"devDependencies": {
"@iconify-json/codicon": "^1.1.37",
"@iconify-json/ph": "^1.1.8",
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@tauri-apps/cli": "2.0.0",
"@unocss/extractor-svelte": "^0.63.0",
"svelte": "^4.2.19",
"unocss": "^0.63.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@tauri-apps/cli": "2.0.4",
"@unocss/extractor-svelte": "^0.64.0",
"svelte": "^5.0.0",
"unocss": "^0.64.0",
"vite": "^5.4.7"
}
}
+25
View File
@@ -1,5 +1,30 @@
# Changelog
## \[2.0.5]
### Dependencies
- Upgraded to `clipboard-manager@2.0.2`
- Upgraded to `log-plugin@2.0.2`
## \[2.0.4]
### Dependencies
- Upgraded to `fs@2.0.3`
- Upgraded to `dialog@2.0.3`
- Upgraded to `http@2.0.3`
## \[2.0.3]
### Dependencies
- Upgraded to `dialog@2.0.2`
- Upgraded to `fs@2.0.2`
- Upgraded to `http@2.0.2`
- Upgraded to `shell@2.0.2`
- Upgraded to `store@2.1.0`
## \[2.0.2]
- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
+8 -8
View File
@@ -1,7 +1,7 @@
[package]
name = "api"
publish = false
version = "2.0.2"
version = "2.0.5"
description = "An example Tauri Application showcasing the api"
edition = "2021"
rust-version = { workspace = true }
@@ -19,22 +19,22 @@ serde_json = { workspace = true }
serde = { workspace = true }
tiny_http = "0.12"
log = { workspace = true }
tauri-plugin-log = { path = "../../../plugins/log", version = "2.0.1" }
tauri-plugin-fs = { path = "../../../plugins/fs", version = "2.0.1", features = [
tauri-plugin-log = { path = "../../../plugins/log", version = "2.0.2" }
tauri-plugin-fs = { path = "../../../plugins/fs", version = "2.0.3", features = [
"watch",
] }
tauri-plugin-clipboard-manager = { path = "../../../plugins/clipboard-manager", version = "2.0.1" }
tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.0.1" }
tauri-plugin-clipboard-manager = { path = "../../../plugins/clipboard-manager", version = "2.0.2" }
tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.0.3" }
tauri-plugin-http = { path = "../../../plugins/http", features = [
"multipart",
], version = "2.0.1" }
], version = "2.0.3" }
tauri-plugin-notification = { path = "../../../plugins/notification", version = "2.0.1", features = [
"windows7-compat",
] }
tauri-plugin-os = { path = "../../../plugins/os", version = "2.0.1" }
tauri-plugin-process = { path = "../../../plugins/process", version = "2.0.1" }
tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.0.1" }
tauri-plugin-store = { path = "../../../plugins/store", version = "2.0.1" }
tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.0.2" }
tauri-plugin-store = { path = "../../../plugins/store", version = "2.1.0" }
[dependencies.tauri]
workspace = true
@@ -53,6 +53,7 @@
}
]
},
"shell:allow-open",
"shell:allow-kill",
"shell:allow-stdin-write",
"process:allow-exit",
@@ -79,10 +80,6 @@
],
"deny": ["$APPDATA/db/*.stronghold"]
},
"store:allow-entries",
"store:allow-get",
"store:allow-set",
"store:allow-save",
"store:allow-load"
"store:default"
]
}
@@ -9,6 +9,8 @@
"updater:default",
"global-shortcut:allow-unregister",
"global-shortcut:allow-register",
"global-shortcut:allow-unregister-all"
"global-shortcut:allow-unregister-all",
{ "identifier": "fs:allow-watch", "allow": ["*", "**/*"] },
"fs:allow-unwatch"
]
}
+4 -1
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
@@ -18,7 +19,7 @@
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$USER_HOME$/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tauri-2.0.0-rc.7/mobile/android" />
<option value="$USER_HOME$/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tauri-2.0.2/mobile/android" />
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/buildSrc" />
@@ -27,6 +28,8 @@
<option value="$PROJECT_DIR$/../../../../../plugins/clipboard-manager/android" />
<option value="$PROJECT_DIR$/../../../../../plugins/dialog/android" />
<option value="$PROJECT_DIR$/../../../../../plugins/fs/android" />
<option value="$PROJECT_DIR$/../../../../../plugins/geolocation/android" />
<option value="$PROJECT_DIR$/../../../../../plugins/haptics/android" />
<option value="$PROJECT_DIR$/../../../../../plugins/nfc/android" />
<option value="$PROJECT_DIR$/../../../../../plugins/notification/android" />
<option value="$PROJECT_DIR$/../../../../../plugins/shell/android" />
+2 -1
View File
@@ -67,7 +67,8 @@ pub fn run() {
.user_agent(&format!("Tauri API - {}", std::env::consts::OS))
.title("Tauri API Validation")
.inner_size(1000., 800.)
.min_inner_size(600., 400.);
.min_inner_size(600., 400.)
.visible(false);
}
#[cfg(target_os = "windows")]
+44 -41
View File
@@ -205,7 +205,7 @@
if (consoleTextEl) consoleTextEl.scrollTop = consoleTextEl.scrollHeight
}
// this function is renders HTML without sanitizing it so it's insecure
// 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) => [
@@ -334,42 +334,46 @@
children:h-100% children:w-12 children:inline-flex
children:items-center children:justify-center"
>
<span
<button
aria-label="Toggle dark mode"
title={isDark ? 'Switch to Light mode' : 'Switch to Dark mode'}
class="hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
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 class="i-ph-sun"></div>
{:else}
<div class="i-ph-moon" />
<div class="i-ph-moon"></div>
{/if}
</span>
<span
</button>
<button
aria-label="Minimize window"
title="Minimize"
class="hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
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" />
</span>
<span
<div class="i-codicon-chrome-minimize"></div>
</button>
<button
aria-label="Maximize window"
title={isWindowMaximized ? 'Restore' : 'Maximize'}
class="hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
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 class="i-codicon-chrome-restore"></div>
{:else}
<div class="i-codicon-chrome-maximize" />
<div class="i-codicon-chrome-maximize"></div>
{/if}
</span>
<span
</button>
<button
aria-label="Close window"
title="Close"
class="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"
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" />
</span>
<div class="i-codicon-chrome-close"></div>
</button>
</span>
</div>
{/if}
@@ -377,13 +381,13 @@
<!-- Sidebar toggle, only visible on small screens -->
<div
id="sidebarToggle"
class="z-2000 sidebar-toggle display-none lt-sm:flex justify-center absolute items-center w-8 h-8 rd-8
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"
>
{#if isSideBarOpen}
<span class="i-codicon-close animate-duration-300ms animate-fade-in" />
<span class="i-codicon-close animate-duration-300ms animate-fade-in"></span>
{:else}
<span class="i-codicon-menu animate-duration-300ms animate-fade-in" />
<span class="i-codicon-menu animate-duration-300ms animate-fade-in"></span>
{/if}
</div>
@@ -395,24 +399,21 @@
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"
>
<img
on:click={() => open('https://tauri.app/')}
class="self-center p-7 cursor-pointer"
src="tauri_logo.png"
alt="Tauri logo"
/>
<a href="https://tauri.app" target="_blank">
<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 class="i-ph-sun"></div>
{:else}
Switch to Dark mode
<div class="i-ph-moon" />
<div class="i-ph-moon"></div>
{/if}
</a>
<br />
<div class="bg-white/5 h-2px" />
<div class="bg-white/5 h-2px"></div>
<br />
{/if}
@@ -422,7 +423,7 @@
href="https://tauri.app/v1/guides/"
>
Documentation
<span class="i-codicon-link-external" />
<span class="i-codicon-link-external"></span>
</a>
<a
class="nv justify-between h-8"
@@ -430,7 +431,7 @@
href="https://github.com/tauri-apps/tauri"
>
GitHub
<span class="i-codicon-link-external" />
<span class="i-codicon-link-external"></span>
</a>
<a
class="nv justify-between h-8"
@@ -438,10 +439,10 @@
href="https://github.com/tauri-apps/tauri/tree/dev/examples/api"
>
Source
<span class="i-codicon-link-external" />
<span class="i-codicon-link-external"></span>
</a>
<br />
<div class="bg-white/5 h-2px" />
<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"
@@ -456,7 +457,7 @@
isSideBarOpen = false
}}
>
<div class="{view.icon} mr-2" />
<div class="{view.icon} mr-2"></div>
<p>{view.label}</p></a
>
{/if}
@@ -485,21 +486,23 @@
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}
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>
<div
class="cursor-pointer h-85% rd-1 p-1 flex justify-center items-center
<button
aria-label="Clear Console"
class="cursor-pointer h-85% rd-1 p-1 flex justify-center items-center border-none bg-inherit
hover:bg-hoverOverlay dark:hover:bg-darkHoverOverlay
active:bg-hoverOverlay/25 dark:active:bg-darkHoverOverlay/25
"
on:click={clear}
>
<div class="i-codicon-clear-all" />
</div>
<div class="i-codicon-clear-all"></div>
</button>
</div>
<div
bind:this={consoleTextEl}
+2 -1
View File
@@ -5,8 +5,9 @@
import 'uno.css'
import './app.css'
import App from './App.svelte'
import { mount } from 'svelte'
const app = new App({
const app = mount(App, {
target: document.querySelector('#app')
})
+5 -5
View File
@@ -1,14 +1,14 @@
<script>
import { getMatches } from "@tauri-apps/plugin-cli";
import { getMatches } from '@tauri-apps/plugin-cli'
export let onMessage;
export let onMessage
function cliMatches() {
getMatches().then(onMessage).catch(onMessage);
getMatches().then(onMessage).catch(onMessage)
}
</script>
<p>
<div>
This binary can be run from the terminal and takes the following arguments:
<code class="code-block flex flex-wrap my-2">
<pre>
@@ -17,7 +17,7 @@
--verbose</pre>
</code>
Additionally, it has a <code>update --background</code> subcommand.
</p>
</div>
<br />
<div class="note">
Note that the arguments are only parsed, not implemented.
+35 -35
View File
@@ -1,69 +1,69 @@
<script>
import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
import { JsonView } from "@zerodevx/svelte-json-view";
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
import { JsonView } from '@zerodevx/svelte-json-view'
let httpMethod = "GET";
let httpBody = "";
let httpMethod = 'GET'
let httpBody = ''
export let onMessage;
export let onMessage
async function makeHttpRequest() {
let method = httpMethod || "GET";
let method = httpMethod || 'GET'
const options = {
method: method || "GET",
headers: {},
};
method: method || 'GET',
headers: {}
}
let bodyType;
let bodyType
if (method !== "GET") {
options.body = httpBody;
if (method !== 'GET') {
options.body = httpBody
if (
(httpBody.startsWith("{") && httpBody.endsWith("}")) ||
(httpBody.startsWith("[") && httpBody.endsWith("]"))
(httpBody.startsWith('{') && httpBody.endsWith('}')) ||
(httpBody.startsWith('[') && httpBody.endsWith(']'))
) {
options.headers["Content-Type"] = "application/json";
bodyType = "json";
} else if (httpBody !== "") {
bodyType = "text";
options.headers['Content-Type'] = 'application/json'
bodyType = 'json'
} else if (httpBody !== '') {
bodyType = 'text'
}
}
const response = await tauriFetch("http://localhost:3003", options);
const response = await tauriFetch('http://localhost:3003', options)
const body =
bodyType === "json" ? await response.json() : await response.text();
bodyType === 'json' ? await response.json() : await response.text()
onMessage({
url: response.url,
status: response.status,
ok: response.ok,
headers: Object.fromEntries(response.headers.entries()),
body,
});
body
})
}
/// http form
let foo = "baz";
let bar = "qux";
let result = null;
let foo = 'baz'
let bar = 'qux'
let result = null
async function doPost() {
const form = new FormData();
form.append("foo", foo);
form.append("bar", bar);
const response = await tauriFetch("http://localhost:3003/tauri", {
method: "POST",
body: form,
});
const form = new FormData()
form.append('foo', foo)
form.append('bar', bar)
const response = await tauriFetch('http://localhost:3003/tauri', {
method: 'POST',
body: form
})
result = {
url: response.url,
status: response.status,
ok: response.ok,
headers: Object.fromEntries(response.headers.entries()),
body: await response.text(),
};
body: await response.text()
}
}
</script>
@@ -82,7 +82,7 @@
placeholder="Request body"
rows="5"
bind:value={httpBody}
/>
></textarea>
<br />
<button class="btn" id="make-request"> Make request </button>
</form>
+29 -21
View File
@@ -1,38 +1,44 @@
<script>
import { scan, checkPermissions, requestPermissions, Format, cancel } from "@tauri-apps/plugin-barcode-scanner";
import {
scan,
checkPermissions,
requestPermissions,
Format,
cancel
} from '@tauri-apps/plugin-barcode-scanner'
export let onMessage;
export let onMessage
let scanning = false;
let windowed = true;
let formats = [Format.QRCode];
const supportedFormats = [Format.QRCode, Format.EAN13];
let scanning = false
let windowed = true
let formats = [Format.QRCode]
const supportedFormats = [Format.QRCode, Format.EAN13]
async function startScan() {
let permission = await checkPermissions();
let permission = await checkPermissions()
if (permission === 'prompt') {
permission = await requestPermissions();
permission = await requestPermissions()
}
if (permission === 'granted') {
scanning = true;
scanning = true
scan({ windowed, formats })
.then((res) => {
scanning = false;
onMessage(res);
scanning = false
onMessage(res)
})
.catch((error) => {
scanning = false;
onMessage(error);
});
scanning = false
onMessage(error)
})
} else {
onMessage('Permission denied')
}
}
async function cancelScan() {
await cancel();
scanning = false;
onMessage("cancelled");
await cancel()
scanning = false
onMessage('cancelled')
}
</script>
@@ -59,11 +65,12 @@
<div class="barcode-scanner--area--container">
<div class="relative">
<p>Aim your camera at a QR code</p>
<button class="btn" type="button" on:click={cancelScan}>Cancel</button>
<button class="btn" type="button" on:click={cancelScan}>Cancel</button
>
</div>
<div class="square surround-cover">
<div class="barcode-scanner--area--outer surround-cover">
<div class="barcode-scanner--area--inner" />
<div class="barcode-scanner--area--inner"></div>
</div>
</div>
</div>
@@ -111,7 +118,7 @@
transition: 0.3s;
}
.square:after {
content: "";
content: '';
top: 0;
display: block;
padding-bottom: 100%;
@@ -141,7 +148,8 @@
width: 100%;
margin: 1rem;
border: 2px solid #fff;
box-shadow: 0px 0px 2px 1px rgb(0 0 0 / 0.5),
box-shadow:
0px 0px 2px 1px rgb(0 0 0 / 0.5),
inset 0px 0px 2px 1px rgb(0 0 0 / 0.5);
border-radius: 1rem;
}
+60 -18
View File
@@ -1,5 +1,5 @@
<script>
import { Store } from "@tauri-apps/plugin-store";
import { LazyStore } from "@tauri-apps/plugin-store";
import { onMount } from "svelte";
export let onMessage;
@@ -7,28 +7,65 @@
let key;
let value;
const store = new Store("cache.json");
let store = new LazyStore("cache.json");
let cache = {};
onMount(async () => {
await store.load();
const values = await store.entries();
for (const [key, value] of values) {
cache[key] = value;
async function refreshEntries() {
try {
const values = await store.entries();
cache = {};
for (const [key, value] of values) {
cache[key] = value;
}
} catch (error) {
onMessage(error);
}
cache = cache;
}
onMount(async () => {
await refreshEntries();
});
function write(key, value) {
store
.set(key, value)
.then(() => store.get(key))
.then((v) => {
cache[key] = v;
async function write(key, value) {
try {
if (value) {
await store.set(key, value);
} else {
await store.delete(key);
}
const v = await store.get(key);
if (v === undefined) {
delete cache[key];
cache = cache;
})
.then(() => store.save())
.catch(onMessage);
} else {
cache[key] = v;
}
} catch (error) {
onMessage(error);
}
}
async function reset() {
try {
await store.reset();
} catch (error) {
onMessage(error);
}
await refreshEntries();
}
async function close() {
try {
await store.close();
onMessage("Store is now closed, any new operations will error out");
} catch (error) {
onMessage(error);
}
}
function reopen() {
store = new LazyStore("cache.json");
onMessage("We made a new `LazyStore` instance, operations will now work");
}
</script>
@@ -44,7 +81,12 @@
<input class="grow input" bind:value />
</div>
<button class="btn" on:click={() => write(key, value)}> Write </button>
<div>
<button class="btn" on:click={() => write(key, value)}>Write</button>
<button class="btn" on:click={() => reset()}>Reset</button>
<button class="btn" on:click={() => close()}>Close</button>
<button class="btn" on:click={() => reopen()}>Re-open</button>
</div>
</div>
<div>
+31 -31
View File
@@ -1,56 +1,56 @@
<script>
import { check } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
import { check } from '@tauri-apps/plugin-updater'
import { relaunch } from '@tauri-apps/plugin-process'
export let onMessage;
export let onMessage
let isChecking, isInstalling, newUpdate;
let isChecking, isInstalling, newUpdate
let totalSize = 0,
downloadedSize = 0;
downloadedSize = 0
async function checkUpdate() {
isChecking = true;
isChecking = true
try {
const update = await check();
onMessage(`Should update: ${update.available}`);
onMessage(update);
const update = await check()
onMessage(`Should update: ${update.available}`)
onMessage(update)
newUpdate = update;
newUpdate = update
} catch (e) {
onMessage(e);
onMessage(e)
} finally {
isChecking = false;
isChecking = false
}
}
async function install() {
isInstalling = true;
downloadedSize = 0;
isInstalling = true
downloadedSize = 0
try {
await newUpdate.downloadAndInstall((downloadProgress) => {
switch (downloadProgress.event) {
case "Started":
totalSize = downloadProgress.data.contentLength;
break;
case "Progress":
downloadedSize += downloadProgress.data.chunkLength;
break;
case "Finished":
break;
case 'Started':
totalSize = downloadProgress.data.contentLength
break
case 'Progress':
downloadedSize += downloadProgress.data.chunkLength
break
case 'Finished':
break
}
});
onMessage("Installation complete, restarting...");
await new Promise((resolve) => setTimeout(resolve, 2000));
await relaunch();
})
onMessage('Installation complete, restarting...')
await new Promise((resolve) => setTimeout(resolve, 2000))
await relaunch()
} catch (e) {
console.error(e);
onMessage(e);
console.error(e)
onMessage(e)
} finally {
isInstalling = false;
isInstalling = false
}
}
$: progress = totalSize ? Math.round((downloadedSize / totalSize) * 100) : 0;
$: progress = totalSize ? Math.round((downloadedSize / totalSize) * 100) : 0
</script>
<div class="flex children:grow children:h10">
@@ -61,7 +61,7 @@
{:else}
<div class="progress">
<span>{progress}%</span>
<div class="progress-bar" style="width: {progress}%" />
<div class="progress-bar" style="width: {progress}%"></div>
</div>
{/if}
</div>
+6 -6
View File
@@ -10,20 +10,20 @@
"format:check": "prettier --check ."
},
"devDependencies": {
"@eslint/js": "9.11.1",
"@eslint/js": "9.14.0",
"@rollup/plugin-node-resolve": "15.3.0",
"@rollup/plugin-terser": "0.4.4",
"@rollup/plugin-typescript": "11.1.6",
"@types/eslint__js": "8.42.3",
"covector": "^0.12.3",
"eslint": "9.11.1",
"eslint": "9.14.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-security": "3.0.1",
"prettier": "3.3.3",
"rollup": "4.22.4",
"tslib": "2.7.0",
"typescript": "5.6.2",
"typescript-eslint": "8.8.0"
"rollup": "4.24.4",
"tslib": "2.8.1",
"typescript": "5.6.3",
"typescript-eslint": "8.13.0"
},
"resolutions": {
"semver": ">=7.5.2",
+4
View File
@@ -1,5 +1,9 @@
# Changelog
## \[2.0.2]
- [`d57df4de`](https://github.com/tauri-apps/plugins-workspace/commit/d57df4debe7c75cfbd6d6558fff1beb07dbee54c) ([#1986](https://github.com/tauri-apps/plugins-workspace/pull/1986) by [@RikaKagurasaka](https://github.com/tauri-apps/plugins-workspace/../../RikaKagurasaka)) Fix that `read_image` wrongly set the image rgba data with binary PNG data.
## \[2.0.1]
- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
+1 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-clipboard-manager"
version = "2.0.1"
version = "2.0.2"
description = "Read and write to the system clipboard."
edition = { workspace = true }
authors = { workspace = true }
@@ -37,4 +37,3 @@ tauri = { workspace = true, features = ["wry"] }
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
arboard = "3"
image = "0.25"
+3 -9
View File
@@ -3,7 +3,6 @@
// SPDX-License-Identifier: MIT
use arboard::ImageData;
use image::ImageEncoder;
use serde::de::DeserializeOwned;
use tauri::{image::Image, plugin::PluginApi, AppHandle, Runtime};
@@ -85,16 +84,11 @@ impl<R: Runtime> Clipboard<R> {
match &self.clipboard {
Ok(clipboard) => {
let image = clipboard.lock().unwrap().get_image()?;
let mut buffer: Vec<u8> = Vec::new();
image::codecs::png::PngEncoder::new(&mut buffer).write_image(
&image.bytes,
let image = Image::new_owned(
image.bytes.to_vec(),
image.width as u32,
image.height as u32,
image::ExtendedColorType::Rgba8,
)?;
let image = Image::new_owned(buffer, image.width as u32, image.height as u32);
);
Ok(image)
}
Err(e) => Err(crate::Error::Clipboard(e.to_string())),
-3
View File
@@ -15,9 +15,6 @@ pub enum Error {
Clipboard(String),
#[error(transparent)]
Tauri(#[from] tauri::Error),
#[cfg(desktop)]
#[error("invalid image: {0}")]
Image(#[from] image::ImageError),
}
impl Serialize for Error {
+2 -2
View File
@@ -10,11 +10,11 @@
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "2.0.1",
"@tauri-apps/api": "2.0.3",
"@tauri-apps/plugin-deep-link": "2.0.0"
},
"devDependencies": {
"@tauri-apps/cli": "2.0.0",
"@tauri-apps/cli": "2.0.4",
"typescript": "^5.2.2",
"vite": "^5.4.7"
}
+13
View File
@@ -1,5 +1,16 @@
# Changelog
## \[2.0.3]
### Dependencies
- Upgraded to `fs@2.0.3`
## \[2.0.1]
- [`2302c2db`](https://github.com/tauri-apps/plugins-workspace/commit/2302c2db1c49673e61dcbda8cdb01b2c57e9ba6f) ([#1910](https://github.com/tauri-apps/plugins-workspace/pull/1910) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix `ask` and `confirm` not using system button texts
- [`aee14ed4`](https://github.com/tauri-apps/plugins-workspace/commit/aee14ed4261cdedc4ed7cc2686f01f437859a5c7) ([#1892](https://github.com/tauri-apps/plugins-workspace/pull/1892) by [@nashaofu](https://github.com/tauri-apps/plugins-workspace/../../nashaofu)) Set `save` dialog mime type from the `filters` extensions on Android.
## \[2.0.1]
- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
@@ -288,3 +299,5 @@
pull/371)) First v2 alpha release!
lpha release!
pull/371)) First v2 alpha release!
lease!
pull/371)) First v2 alpha release!
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-dialog"
version = "2.0.1"
version = "2.0.3"
description = "Native system dialogs for opening and saving files along with message dialogs on your Tauri application."
edition = { workspace = true }
authors = { workspace = true }
@@ -34,7 +34,7 @@ tauri = { workspace = true }
log = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }
tauri-plugin-fs = { path = "../fs", version = "2.0.1" }
tauri-plugin-fs = { path = "../fs", version = "2.0.3" }
[target.'cfg(target_os = "ios")'.dependencies]
tauri = { workspace = true, features = ["wry"] }
@@ -10,6 +10,7 @@ import android.content.Intent
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.webkit.MimeTypeMap
import androidx.activity.result.ActivityResult
import app.tauri.Logger
import app.tauri.annotation.ActivityCallback
@@ -43,6 +44,7 @@ class MessageOptions {
@InvokeArg
class SaveFileDialogOptions {
var fileName: String? = null
lateinit var filters: Array<Filter>
}
@TauriPlugin
@@ -57,20 +59,7 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) {
val intent = if (parsedTypes.isNotEmpty()) {
val intent = Intent(Intent.ACTION_PICK)
intent.putExtra(Intent.EXTRA_MIME_TYPES, parsedTypes)
var uniqueMimeType = true
var mimeKind: String? = null
for (mime in parsedTypes) {
val kind = mime.split("/")[0]
if (mimeKind == null) {
mimeKind = kind
} else if (mimeKind != kind) {
uniqueMimeType = false
}
}
intent.type = if (uniqueMimeType) Intent.normalizeMimeType("$mimeKind/*") else "*/*"
setIntentMimeTypes(intent, parsedTypes)
intent
} else {
val intent = Intent(Intent.ACTION_GET_CONTENT)
@@ -130,12 +119,46 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) {
private fun parseFiltersOption(filters: Array<Filter>): Array<String> {
val mimeTypes = mutableListOf<String>()
for (filter in filters) {
for (mime in filter.extensions) {
mimeTypes.add(if (mime == "text/csv") "text/comma-separated-values" else mime)
for (ext in filter.extensions) {
if (ext.contains('/')) {
mimeTypes.add(if (ext == "text/csv") "text/comma-separated-values" else ext)
} else {
MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)?.let {
mimeTypes.add(it)
}
}
}
}
return mimeTypes.toTypedArray()
}
private fun setIntentMimeTypes(intent: Intent, mimeTypes: Array<String>) {
if (mimeTypes.isNotEmpty()) {
var uniqueMimeKind = true
var mimeKind: String? = null
for (mime in mimeTypes) {
val kind = mime.split("/")[0]
if (mimeKind == null) {
mimeKind = kind
} else if (mimeKind != kind) {
uniqueMimeKind = false
}
}
if (uniqueMimeKind) {
if (mimeTypes.size > 1) {
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
intent.type = Intent.normalizeMimeType("$mimeKind/*")
} else {
intent.type = mimeTypes[0]
}
} else {
intent.type = "*/*"
}
} else {
intent.type = "*/*"
}
}
@Command
fun showMessageDialog(invoke: Invoke) {
@@ -187,10 +210,12 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) {
fun saveFileDialog(invoke: Invoke) {
try {
val args = invoke.parseArgs(SaveFileDialogOptions::class.java)
val parsedTypes = parseFiltersOption(args.filters)
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
setIntentMimeTypes(intent, parsedTypes)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.setType("text/plain")
intent.putExtra(Intent.EXTRA_TITLE, args.fileName ?: "")
startActivityForResult(invoke, intent, "saveFileDialogResult")
} catch (ex: Exception) {
+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)}return"function"==typeof SuppressedError&&SuppressedError,t.ask=async function(t,e){const i="string"==typeof e?{title:e}:e;return await n("plugin:dialog|ask",{message:t.toString(),title:i?.title?.toString(),kind:i?.kind,okButtonLabel:i?.okLabel?.toString()??"Yes",cancelButtonLabel:i?.cancelLabel?.toString()??"No"})},t.confirm=async function(t,e){const i="string"==typeof e?{title:e}:e;return await n("plugin:dialog|confirm",{message:t.toString(),title:i?.title?.toString(),kind:i?.kind,okButtonLabel:i?.okLabel?.toString()??"Ok",cancelButtonLabel:i?.cancelLabel?.toString()??"Cancel"})},t.message=async function(t,e){const i="string"==typeof e?{title:e}:e;await n("plugin:dialog|message",{message:t.toString(),title:i?.title?.toString(),kind:i?.kind,okButtonLabel:i?.okLabel?.toString()})},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(t){"use strict";async function n(t,n={},e){return window.__TAURI_INTERNALS__.invoke(t,n,e)}return"function"==typeof SuppressedError&&SuppressedError,t.ask=async function(t,e){const i="string"==typeof e?{title:e}:e;return await n("plugin:dialog|ask",{message:t.toString(),title:i?.title?.toString(),kind:i?.kind,yesButtonLabel:i?.okLabel?.toString(),noButtonLabel:i?.cancelLabel?.toString()})},t.confirm=async function(t,e){const i="string"==typeof e?{title:e}:e;return await n("plugin:dialog|confirm",{message:t.toString(),title:i?.title?.toString(),kind:i?.kind,okButtonLabel:i?.okLabel?.toString(),cancelButtonLabel:i?.cancelLabel?.toString()})},t.message=async function(t,e){const i="string"==typeof e?{title:e}:e;await n("plugin:dialog|message",{message:t.toString(),title:i?.title?.toString(),kind:i?.kind,okButtonLabel:i?.okLabel?.toString()})},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__})}
+4 -4
View File
@@ -257,8 +257,8 @@ async function ask(
message: message.toString(),
title: opts?.title?.toString(),
kind: opts?.kind,
okButtonLabel: opts?.okLabel?.toString() ?? 'Yes',
cancelButtonLabel: opts?.cancelLabel?.toString() ?? 'No'
yesButtonLabel: opts?.okLabel?.toString(),
noButtonLabel: opts?.cancelLabel?.toString()
})
}
@@ -287,8 +287,8 @@ async function confirm(
message: message.toString(),
title: opts?.title?.toString(),
kind: opts?.kind,
okButtonLabel: opts?.okLabel?.toString() ?? 'Ok',
cancelButtonLabel: opts?.cancelLabel?.toString() ?? 'Cancel'
okButtonLabel: opts?.okLabel?.toString(),
cancelButtonLabel: opts?.cancelLabel?.toString()
})
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-dialog",
"version": "2.0.0",
"version": "2.0.1",
"license": "MIT OR Apache-2.0",
"authors": [
"Tauri Programme within The Commons Conservancy"
+23 -21
View File
@@ -10,7 +10,7 @@ use tauri_plugin_fs::FsExt;
use crate::{
Dialog, FileDialogBuilder, FilePath, MessageDialogButtons, MessageDialogKind, Result, CANCEL,
OK,
NO, OK, YES,
};
#[derive(Serialize)]
@@ -299,8 +299,8 @@ pub(crate) async fn ask<R: Runtime>(
title: Option<String>,
message: String,
kind: Option<MessageDialogKind>,
ok_button_label: Option<String>,
cancel_button_label: Option<String>,
yes_button_label: Option<String>,
no_button_label: Option<String>,
) -> Result<bool> {
Ok(message_dialog(
window,
@@ -308,7 +308,16 @@ pub(crate) async fn ask<R: Runtime>(
title,
message,
kind,
get_ok_cancel_type(ok_button_label, cancel_button_label),
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
},
))
}
@@ -328,22 +337,15 @@ pub(crate) async fn confirm<R: Runtime>(
title,
message,
kind,
get_ok_cancel_type(ok_button_label, cancel_button_label),
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
},
))
}
fn get_ok_cancel_type(
ok_button_label: Option<String>,
cancel_button_label: Option<String>,
) -> MessageDialogButtons {
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
}
}
+1
View File
@@ -112,6 +112,7 @@ impl From<MessageDialogButtons> for rfd::MessageButtons {
match value {
MessageDialogButtons::Ok => Self::Ok,
MessageDialogButtons::OkCancel => Self::OkCancel,
MessageDialogButtons::YesNo => Self::YesNo,
MessageDialogButtons::OkCustom(ok) => Self::OkCustom(ok),
MessageDialogButtons::OkCancelCustom(ok, cancel) => Self::OkCancelCustom(ok, cancel),
}
+3
View File
@@ -43,6 +43,8 @@ use mobile::*;
pub(crate) const OK: &str = "Ok";
pub(crate) const CANCEL: &str = "Cancel";
pub(crate) const YES: &str = "Yes";
pub(crate) const NO: &str = "No";
macro_rules! blocking_fn {
($self:ident, $fn:ident) => {{
@@ -236,6 +238,7 @@ impl<R: Runtime> MessageDialogBuilder<R> {
let (ok_button_label, cancel_button_label) = match &self.buttons {
MessageDialogButtons::Ok => (Some(OK), None),
MessageDialogButtons::OkCancel => (Some(OK), Some(CANCEL)),
MessageDialogButtons::YesNo => (Some(YES), Some(NO)),
MessageDialogButtons::OkCustom(ok) => (Some(ok.as_str()), Some(CANCEL)),
MessageDialogButtons::OkCancelCustom(ok, cancel) => {
(Some(ok.as_str()), Some(cancel.as_str()))
+2
View File
@@ -59,6 +59,8 @@ pub enum MessageDialogButtons {
Ok,
/// 2 buttons `Ok` and `Cancel` with OS default dialog texts
OkCancel,
/// 2 buttons `Yes` and `No` with OS default dialog texts
YesNo,
/// A single `Ok` button with custom text
OkCustom(String),
/// 2 buttons `Ok` and `Cancel` with custom texts
+12
View File
@@ -1,5 +1,17 @@
# Changelog
## \[2.0.2]
- [`77149dc4`](https://github.com/tauri-apps/plugins-workspace/commit/77149dc4320d26b413e4a6bbe82c654367c51b32) ([#1965](https://github.com/tauri-apps/plugins-workspace/pull/1965) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Fix `writeTextFile` converting UTF-8 characters (for example `äöü`) in the given path into replacement character (``)
## \[2.0.3]
- [`14cee64c`](https://github.com/tauri-apps/plugins-workspace/commit/14cee64c82a72655ae6a4ac0892736a2959dbda5) ([#1958](https://github.com/tauri-apps/plugins-workspace/pull/1958) by [@bWanShiTong](https://github.com/tauri-apps/plugins-workspace/../../bWanShiTong)) Fix compilation on targets with pointer width of `16` or `32`
## \[2.0.1]
- [`ae802456`](https://github.com/tauri-apps/plugins-workspace/commit/ae8024565f074f313084777c8b10d1b5e3bbe220) ([#1950](https://github.com/tauri-apps/plugins-workspace/pull/1950) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Improve performance of the `FileHandle.read` and `writeTextFile` APIs.
## \[2.0.1]
- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
+7 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-fs"
version = "2.0.1"
version = "2.0.3"
description = "Access the file system."
authors = { workspace = true }
license = { workspace = true }
@@ -35,8 +35,12 @@ url = { workspace = true }
anyhow = "1"
uuid = { version = "1", features = ["v4"] }
glob = "0.3"
notify = { version = "6", optional = true, features = ["serde"] }
notify-debouncer-full = { version = "0.3", optional = true }
# TODO: Remove `serialization-compat-6` in v3
notify = { version = "7", optional = true, features = [
"serde",
"serialization-compat-6",
] }
notify-debouncer-full = { version = "0.4", optional = true }
dunce = { workspace = true }
percent-encoding = "2"
File diff suppressed because one or more lines are too long
+36 -6
View File
@@ -243,6 +243,25 @@ function parseFileInfo(r: UnparsedFileInfo): FileInfo {
}
}
// https://mstn.github.io/2018/06/08/fixed-size-arrays-in-typescript/
type FixedSizeArray<T, N extends number> = ReadonlyArray<T> & {
length: N
}
// https://gist.github.com/zapthedingbat/38ebfbedd98396624e5b5f2ff462611d
/** Converts a big-endian eight byte array to number */
function fromBytes(buffer: FixedSizeArray<number, 8>): number {
const bytes = new Uint8ClampedArray(buffer)
const size = bytes.byteLength
let x = 0
for (let i = 0; i < size; i++) {
const byte = bytes[i]
x *= 0x100
x += byte
}
return x
}
/**
* The Tauri abstraction for reading and writing files.
*
@@ -285,12 +304,20 @@ class FileHandle extends Resource {
return 0
}
const [data, nread] = await invoke<[number[], number]>('plugin:fs|read', {
const data = await invoke<ArrayBuffer | number[]>('plugin:fs|read', {
rid: this.rid,
len: buffer.byteLength
})
buffer.set(data)
// Rust side will never return an empty array for this command and
// ensure there is at least 8 elements there.
//
// This is an optimization to include the number of read bytes (as bigendian bytes)
// at the end of returned array to avoid serialization overhead of separate values.
const nread = fromBytes(data.slice(-8) as FixedSizeArray<number, 8>)
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data
buffer.set(bytes.slice(0, bytes.length - 8))
return nread === 0 ? null : nread
}
@@ -1041,10 +1068,13 @@ async function writeTextFile(
throw new TypeError('Must be a file URL.')
}
await invoke('plugin:fs|write_text_file', {
path: path instanceof URL ? path.toString() : path,
data,
options
const encoder = new TextEncoder()
await invoke('plugin:fs|write_text_file', encoder.encode(data), {
headers: {
path: encodeURIComponent(path instanceof URL ? path.toString() : path),
options: JSON.stringify(options)
}
})
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-fs",
"version": "2.0.0",
"version": "2.0.2",
"description": "Access the file system.",
"license": "MIT OR Apache-2.0",
"authors": [
@@ -24,6 +24,7 @@ This default permission set prevents access to critical components
of the Tauri application by default.
On Windows the webview data folder access is denied.
#### Included permissions within this default permission set:
- `create-app-specific-dirs`
+1
View File
@@ -26,6 +26,7 @@ This default permission set prevents access to critical components
of the Tauri application by default.
On Windows the webview data folder access is denied.
#### Included permissions within this default permission set:
"""
permissions = [
"create-app-specific-dirs",
+1 -1
View File
@@ -1665,7 +1665,7 @@
"const": "create-app-specific-dirs"
},
{
"description": "This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n",
"description": "This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n#### Included permissions within this default permission set:\n",
"type": "string",
"const": "default"
},
+63 -50
View File
@@ -9,7 +9,7 @@ use tauri::{
ipc::{CommandScope, GlobalScope},
path::BaseDirectory,
utils::config::FsScope,
AppHandle, Manager, Resource, ResourceId, Runtime, Webview,
Manager, Resource, ResourceId, Runtime, Webview,
};
use std::{
@@ -301,13 +301,36 @@ pub async fn read_dir<R: Runtime>(
pub async fn read<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
len: u32,
) -> CommandResult<(Vec<u8>, usize)> {
let mut data = vec![0; len as usize];
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))
.map_err(|e| format!("faied to read bytes from file with error: {e}"))?;
Ok((data, nread))
// This is an optimization to include the number of read bytes (as bigendian bytes)
// at the end of returned vector so we can use `tauri::ipc::Response`
// and avoid serialization overhead of separate values.
#[cfg(target_pointer_width = "16")]
let nread = {
let nread = nread.to_be_bytes();
let mut out = [0; 8];
out[6..].copy_from_slice(&nread);
out
};
#[cfg(target_pointer_width = "32")]
let nread = {
let nread = nread.to_be_bytes();
let mut out = [0; 8];
out[4..].copy_from_slice(&nread);
out
};
#[cfg(target_pointer_width = "64")]
let nread = nread.to_be_bytes();
data.extend(nread);
Ok(tauri::ipc::Response::new(data))
}
#[tauri::command]
@@ -783,10 +806,34 @@ fn write_file_inner<R: Runtime>(
webview: Webview<R>,
global_scope: &GlobalScope<Entry>,
command_scope: &CommandScope<Entry>,
path: SafeFilePath,
data: &[u8],
options: Option<WriteFileOptions>,
request: tauri::ipc::Request<'_>,
) -> CommandResult<()> {
let data = match request.body() {
tauri::ipc::InvokeBody::Raw(data) => Cow::Borrowed(data),
tauri::ipc::InvokeBody::Json(serde_json::Value::Array(data)) => Cow::Owned(
data.iter()
.flat_map(|v| v.as_number().and_then(|v| v.as_u64().map(|v| v as u8)))
.collect(),
),
_ => return Err(anyhow::anyhow!("unexpected invoke body").into()),
};
let path = request
.headers()
.get("path")
.ok_or_else(|| anyhow::anyhow!("missing file path").into())
.and_then(|p| {
percent_encoding::percent_decode(p.as_ref())
.decode_utf8()
.map_err(|_| anyhow::anyhow!("path is not a valid UTF-8").into())
})
.and_then(|p| SafeFilePath::from_str(&p).map_err(CommandError::from))?;
let options: Option<WriteFileOptions> = request
.headers()
.get("options")
.and_then(|p| p.to_str().ok())
.and_then(|opts| serde_json::from_str(opts).ok());
let (mut file, path) = resolve_file(
&webview,
global_scope,
@@ -823,7 +870,7 @@ fn write_file_inner<R: Runtime>(
},
)?;
file.write_all(data)
file.write_all(&data)
.map_err(|e| {
format!(
"failed to write bytes to file at path: {} with error: {e}",
@@ -840,52 +887,18 @@ pub async fn write_file<R: Runtime>(
command_scope: CommandScope<Entry>,
request: tauri::ipc::Request<'_>,
) -> CommandResult<()> {
let data = match request.body() {
tauri::ipc::InvokeBody::Raw(data) => Cow::Borrowed(data),
tauri::ipc::InvokeBody::Json(serde_json::Value::Array(data)) => Cow::Owned(
data.iter()
.flat_map(|v| v.as_number().and_then(|v| v.as_u64().map(|v| v as u8)))
.collect(),
),
_ => return Err(anyhow::anyhow!("unexpected invoke body").into()),
};
let path = request
.headers()
.get("path")
.ok_or_else(|| anyhow::anyhow!("missing file path").into())
.and_then(|p| {
percent_encoding::percent_decode(p.as_ref())
.decode_utf8()
.map_err(|_| anyhow::anyhow!("path is not a valid UTF-8").into())
})
.and_then(|p| SafeFilePath::from_str(&p).map_err(CommandError::from))?;
let options = request
.headers()
.get("options")
.and_then(|p| p.to_str().ok())
.and_then(|opts| serde_json::from_str(opts).ok());
write_file_inner(webview, &global_scope, &command_scope, path, &data, options)
write_file_inner(webview, &global_scope, &command_scope, request)
}
// TODO, in v3, remove this command and rely on `write_file` command only
#[tauri::command]
pub async fn write_text_file<R: Runtime>(
#[allow(unused)] app: AppHandle<R>,
#[allow(unused)] webview: Webview<R>,
#[allow(unused)] global_scope: GlobalScope<Entry>,
#[allow(unused)] command_scope: CommandScope<Entry>,
path: SafeFilePath,
data: String,
#[allow(unused)] options: Option<WriteFileOptions>,
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
request: tauri::ipc::Request<'_>,
) -> CommandResult<()> {
write_file_inner(
webview,
&global_scope,
&command_scope,
path,
data.as_bytes(),
options,
)
write_file_inner(webview, &global_scope, &command_scope, request)
}
#[tauri::command]
+6 -7
View File
@@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
use notify_debouncer_full::{new_debouncer, DebounceEventResult, Debouncer, FileIdMap};
use notify_debouncer_full::{new_debouncer, DebounceEventResult, Debouncer, RecommendedCache};
use serde::Deserialize;
use tauri::{
ipc::{Channel, CommandScope, GlobalScope},
@@ -47,7 +47,7 @@ impl WatcherResource {
impl Resource for WatcherResource {}
enum WatcherKind {
Debouncer(Debouncer<RecommendedWatcher, FileIdMap>),
Debouncer(Debouncer<RecommendedWatcher, RecommendedCache>),
Watcher(RecommendedWatcher),
}
@@ -111,8 +111,7 @@ pub async fn watch<R: Runtime>(
let (tx, rx) = channel();
let mut debouncer = new_debouncer(Duration::from_millis(delay), None, tx)?;
for path in &resolved_paths {
debouncer.watcher().watch(path.as_ref(), recursive_mode)?;
debouncer.cache().add_root(path, recursive_mode);
debouncer.watch(path, recursive_mode)?;
}
watch_debounced(on_event, rx);
WatcherKind::Debouncer(debouncer)
@@ -120,7 +119,7 @@ pub async fn watch<R: Runtime>(
let (tx, rx) = channel();
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
for path in &resolved_paths {
watcher.watch(path.as_ref(), recursive_mode)?;
watcher.watch(path, recursive_mode)?;
}
watch_raw(on_event, rx);
WatcherKind::Watcher(watcher)
@@ -140,14 +139,14 @@ pub async fn unwatch<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> Comman
match &mut watcher.kind {
WatcherKind::Debouncer(ref mut debouncer) => {
for path in &watcher.paths {
debouncer.watcher().unwatch(path.as_ref()).map_err(|e| {
debouncer.unwatch(path).map_err(|e| {
format!("failed to unwatch path: {} with error: {e}", path.display())
})?;
}
}
WatcherKind::Watcher(ref mut w) => {
for path in &watcher.paths {
w.unwatch(path.as_ref()).map_err(|e| {
w.unwatch(path).map_err(|e| {
format!("failed to unwatch path: {} with error: {e}", path.display())
})?;
}
+14
View File
@@ -1,5 +1,16 @@
# Changelog
## \[2.0.3]
### Dependencies
- Upgraded to `fs@2.0.3`
## \[2.0.1]
- [`cfd48b3b`](https://github.com/tauri-apps/plugins-workspace/commit/cfd48b3b2ec0fccfc162197518694ed59ceda22c) ([#1941](https://github.com/tauri-apps/plugins-workspace/pull/1941) by [@Nipsuli](https://github.com/tauri-apps/plugins-workspace/../../Nipsuli)) Allow skipping sending `Origin` header in HTTP requests by setting `Origin` header to an empty string when calling `fetch`.
- [`9b2840db`](https://github.com/tauri-apps/plugins-workspace/commit/9b2840db9464cf08db75806270e4441f0af81e5d) ([#1884](https://github.com/tauri-apps/plugins-workspace/pull/1884) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Retain headers order.
## \[2.0.1]
- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
@@ -286,3 +297,6 @@
ha release!
!
371\)) First v2 alpha release!
lease!
!
371\)) First v2 alpha release!
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-http"
version = "2.0.1"
version = "2.0.3"
description = "Access an HTTP client written in Rust."
edition = { workspace = true }
authors = { workspace = true }
@@ -34,7 +34,7 @@ serde_json = { workspace = true }
tauri = { workspace = true }
thiserror = { workspace = true }
tokio = { version = "1", features = ["sync", "macros"] }
tauri-plugin-fs = { path = "../fs", version = "2.0.1" }
tauri-plugin-fs = { path = "../fs", version = "2.0.3" }
urlpattern = "0.3"
regex = "1"
http = "1"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-http",
"version": "2.0.0",
"version": "2.0.1",
"license": "MIT OR Apache-2.0",
"authors": [
"Tauri Programme within The Commons Conservancy"
+40 -24
View File
@@ -2,9 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc, time::Duration};
use std::{future::Future, pin::Pin, str::FromStr, sync::Arc, time::Duration};
use http::{header, HeaderName, Method, StatusCode};
use http::{header, HeaderMap, HeaderName, HeaderValue, Method, StatusCode};
use reqwest::{redirect::Policy, NoProxy};
use serde::{Deserialize, Serialize};
use tauri::{
@@ -176,7 +176,7 @@ pub async fn fetch<R: Runtime>(
let ClientConfig {
method,
url,
headers,
headers: headers_raw,
data,
connect_timeout,
max_redirections,
@@ -185,7 +185,22 @@ pub async fn fetch<R: Runtime>(
let scheme = url.scheme();
let method = Method::from_bytes(method.as_bytes())?;
let headers: HashMap<String, String> = HashMap::from_iter(headers);
let mut headers = HeaderMap::new();
for (h, v) in headers_raw {
let name = HeaderName::from_str(&h)?;
#[cfg(not(feature = "unsafe-headers"))]
if is_unsafe_header(&name) {
#[cfg(debug_assertions)]
{
eprintln!("[\x1b[33mWARNING\x1b[0m] Skipping {name} header as it is a forbidden header per fetch spec https://fetch.spec.whatwg.org/#terminology-headers");
eprintln!("[\x1b[33mWARNING\x1b[0m] if keeping the header is a desired behavior, you can enable `unsafe-headers` feature flag in your Cargo.toml");
}
continue;
}
headers.append(name, HeaderValue::from_str(&v)?);
}
match scheme {
"http" | "https" => {
@@ -228,45 +243,46 @@ pub async fn fetch<R: Runtime>(
let mut request = builder.build()?.request(method.clone(), url);
for (name, value) in &headers {
let name = HeaderName::from_bytes(name.as_bytes())?;
#[cfg(not(feature = "unsafe-headers"))]
if is_unsafe_header(&name) {
continue;
}
request = request.header(name, value);
}
// POST and PUT requests should always have a 0 length content-length,
// if there is no body. https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
if data.is_none() && matches!(method, Method::POST | Method::PUT) {
request = request.header(header::CONTENT_LENGTH, 0);
headers.append(header::CONTENT_LENGTH, HeaderValue::from_str("0")?);
}
if headers.contains_key(header::RANGE.as_str()) {
if headers.contains_key(header::RANGE) {
// https://fetch.spec.whatwg.org/#http-network-or-cache-fetch step 18
// If httpRequests header list contains `Range`, then append (`Accept-Encoding`, `identity`)
request = request.header(header::ACCEPT_ENCODING, "identity");
headers.append(header::ACCEPT_ENCODING, HeaderValue::from_str("identity")?);
}
if !headers.contains_key(header::USER_AGENT.as_str()) {
request = request.header(header::USER_AGENT, HTTP_USER_AGENT);
if !headers.contains_key(header::USER_AGENT) {
headers.append(header::USER_AGENT, HeaderValue::from_str(HTTP_USER_AGENT)?);
}
if cfg!(feature = "unsafe-headers")
&& !headers.contains_key(header::ORIGIN.as_str())
{
// ensure we have an Origin header set
if cfg!(not(feature = "unsafe-headers")) || !headers.contains_key(header::ORIGIN) {
if let Ok(url) = webview.url() {
request =
request.header(header::ORIGIN, url.origin().ascii_serialization());
headers.append(
header::ORIGIN,
HeaderValue::from_str(&url.origin().ascii_serialization())?,
);
}
}
// In case empty origin is passed, remove it. Some services do not like Origin header
// so this way we can remove it in explicit way. The default behaviour is still to set it
if cfg!(feature = "unsafe-headers")
&& headers.get(header::ORIGIN) == Some(&HeaderValue::from_static(""))
{
headers.remove(header::ORIGIN);
};
if let Some(data) = data {
request = request.body(data);
}
request = request.headers(headers);
let fut = async move { request.send().await.map_err(Into::into) };
let mut resources_table = webview.resources_table();
let rid = resources_table.add_request(Box::pin(fut));
+4
View File
@@ -1,5 +1,9 @@
# Changelog
## \[2.0.2]
- [`606fa08d`](https://github.com/tauri-apps/plugins-workspace/commit/606fa08dae1acd074b961fb360623f4c86f13ee8) ([#1997](https://github.com/tauri-apps/plugins-workspace/pull/1997) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) **Potentially breaking:** Updated `fern` from 0.6 to 0.7. This is technically a breaking change because `fern` is re-exported in `tauri-plugin-log`.
## \[2.0.1]
- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-log"
version = "2.0.1"
version = "2.0.2"
description = "Configurable logging for your Tauri app."
authors = { workspace = true }
license = { workspace = true }
@@ -31,7 +31,7 @@ serde_repr = "0.1"
byte-unit = "5"
log = { workspace = true, features = ["kv_unstable"] }
time = { version = "0.3", features = ["formatting", "local-offset"] }
fern = "0.6"
fern = "0.7"
thiserror = "1"
[target."cfg(target_os = \"android\")".dependencies]
+1 -1
View File
@@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_NOTIFICATION__=function(i){"use strict";function t(i,t,n,e){if("a"===n&&!e)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?i!==t||!e:!t.has(i))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===n?e:"a"===n?e.call(i):e?e.value:t.get(i)}function n(i,t,n,e,o){if("function"==typeof t?i!==t||!o:!t.has(i))throw new TypeError("Cannot write private member to an object whose class did not declare it");return t.set(i,n),n}var e,o,a,r,c,s;"function"==typeof SuppressedError&&SuppressedError;class l{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,e.set(this,(()=>{})),o.set(this,0),a.set(this,{}),this.id=function(i,t=!1){return window.__TAURI_INTERNALS__.transformCallback(i,t)}((({message:i,id:r})=>{if(r===t(this,o,"f")){n(this,o,r+1),t(this,e,"f").call(this,i);const c=Object.keys(t(this,a,"f"));if(c.length>0){let i=r+1;for(const n of c.sort()){if(parseInt(n)!==i)break;{const o=t(this,a,"f")[n];delete t(this,a,"f")[n],t(this,e,"f").call(this,o),i+=1}}n(this,o,i)}}else t(this,a,"f")[r.toString()]=i}))}set onmessage(i){n(this,e,i)}get onmessage(){return t(this,e,"f")}toJSON(){return`__CHANNEL__:${this.id}`}}e=new WeakMap,o=new WeakMap,a=new WeakMap;class u{constructor(i,t,n){this.plugin=i,this.event=t,this.channelId=n}async unregister(){return d(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}}async function f(i,t,n){const e=new l;return e.onmessage=n,d(`plugin:${i}|register_listener`,{event:t,handler:e}).then((()=>new u(i,t,e.id)))}async function d(i,t={},n){return window.__TAURI_INTERNALS__.invoke(i,t,n)}i.ScheduleEvery=void 0,(r=i.ScheduleEvery||(i.ScheduleEvery={})).Year="year",r.Month="month",r.TwoWeeks="twoWeeks",r.Week="week",r.Day="day",r.Hour="hour",r.Minute="minute",r.Second="second";return i.Importance=void 0,(c=i.Importance||(i.Importance={}))[c.None=0]="None",c[c.Min=1]="Min",c[c.Low=2]="Low",c[c.Default=3]="Default",c[c.High=4]="High",i.Visibility=void 0,(s=i.Visibility||(i.Visibility={}))[s.Secret=-1]="Secret",s[s.Private=0]="Private",s[s.Public=1]="Public",i.Schedule=class{static at(i,t=!1,n=!1){return{at:{date:i,repeating:t,allowWhileIdle:n},interval:void 0,every:void 0}}static interval(i,t=!1){return{at:void 0,interval:{interval:i,allowWhileIdle:t},every:void 0}}static every(i,t,n=!1){return{at:void 0,interval:void 0,every:{interval:i,count:t,allowWhileIdle:n}}}},i.active=async function(){return await d("plugin:notification|get_active")},i.cancel=async function(i){await d("plugin:notification|cancel",{notifications:i})},i.cancelAll=async function(){await d("plugin:notification|cancel")},i.channels=async function(){return await d("plugin:notification|listChannels")},i.createChannel=async function(i){await d("plugin:notification|create_channel",{...i})},i.isPermissionGranted=async function(){return"default"!==window.Notification.permission?await Promise.resolve("granted"===window.Notification.permission):await d("plugin:notification|is_permission_granted")},i.onAction=async function(i){return await f("notification","actionPerformed",i)},i.onNotificationReceived=async function(i){return await f("notification","notification",i)},i.pending=async function(){return await d("plugin:notification|get_pending")},i.registerActionTypes=async function(i){await d("plugin:notification|register_action_types",{types:i})},i.removeActive=async function(i){await d("plugin:notification|remove_active",{notifications:i})},i.removeAllActive=async function(){await d("plugin:notification|remove_active")},i.removeChannel=async function(i){await d("plugin:notification|delete_channel",{id:i})},i.requestPermission=async function(){return await window.Notification.requestPermission()},i.sendNotification=function(i){"string"==typeof i?new window.Notification(i):new window.Notification(i.title,i)},i}({});Object.defineProperty(window.__TAURI__,"notification",{value:__TAURI_PLUGIN_NOTIFICATION__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_NOTIFICATION__=function(i){"use strict";function t(i,t,n,e){if("a"===n&&!e)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?i!==t||!e:!t.has(i))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===n?e:"a"===n?e.call(i):e?e.value:t.get(i)}function n(i,t,n,e,o){if("function"==typeof t?i!==t||!o:!t.has(i))throw new TypeError("Cannot write private member to an object whose class did not declare it");return t.set(i,n),n}var e,o,a,r,c,s;"function"==typeof SuppressedError&&SuppressedError;class l{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,e.set(this,(()=>{})),o.set(this,0),a.set(this,{}),this.id=function(i,t=!1){return window.__TAURI_INTERNALS__.transformCallback(i,t)}((({message:i,id:r})=>{if(r===t(this,o,"f")){n(this,o,r+1),t(this,e,"f").call(this,i);const c=Object.keys(t(this,a,"f"));if(c.length>0){let i=r+1;for(const n of c.sort()){if(parseInt(n)!==i)break;{const o=t(this,a,"f")[n];delete t(this,a,"f")[n],t(this,e,"f").call(this,o),i+=1}}n(this,o,i)}}else t(this,a,"f")[r.toString()]=i}))}set onmessage(i){n(this,e,i)}get onmessage(){return t(this,e,"f")}toJSON(){return`__CHANNEL__:${this.id}`}}e=new WeakMap,o=new WeakMap,a=new WeakMap;class u{constructor(i,t,n){this.plugin=i,this.event=t,this.channelId=n}async unregister(){return d(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}}async function f(i,t,n){const e=new l;return e.onmessage=n,d(`plugin:${i}|registerListener`,{event:t,handler:e}).then((()=>new u(i,t,e.id)))}async function d(i,t={},n){return window.__TAURI_INTERNALS__.invoke(i,t,n)}i.ScheduleEvery=void 0,(r=i.ScheduleEvery||(i.ScheduleEvery={})).Year="year",r.Month="month",r.TwoWeeks="twoWeeks",r.Week="week",r.Day="day",r.Hour="hour",r.Minute="minute",r.Second="second";return i.Importance=void 0,(c=i.Importance||(i.Importance={}))[c.None=0]="None",c[c.Min=1]="Min",c[c.Low=2]="Low",c[c.Default=3]="Default",c[c.High=4]="High",i.Visibility=void 0,(s=i.Visibility||(i.Visibility={}))[s.Secret=-1]="Secret",s[s.Private=0]="Private",s[s.Public=1]="Public",i.Schedule=class{static at(i,t=!1,n=!1){return{at:{date:i,repeating:t,allowWhileIdle:n},interval:void 0,every:void 0}}static interval(i,t=!1){return{at:void 0,interval:{interval:i,allowWhileIdle:t},every:void 0}}static every(i,t,n=!1){return{at:void 0,interval:void 0,every:{interval:i,count:t,allowWhileIdle:n}}}},i.active=async function(){return await d("plugin:notification|get_active")},i.cancel=async function(i){await d("plugin:notification|cancel",{notifications:i})},i.cancelAll=async function(){await d("plugin:notification|cancel")},i.channels=async function(){return await d("plugin:notification|listChannels")},i.createChannel=async function(i){await d("plugin:notification|create_channel",{...i})},i.isPermissionGranted=async function(){return"default"!==window.Notification.permission?await Promise.resolve("granted"===window.Notification.permission):await d("plugin:notification|is_permission_granted")},i.onAction=async function(i){return await f("notification","actionPerformed",i)},i.onNotificationReceived=async function(i){return await f("notification","notification",i)},i.pending=async function(){return await d("plugin:notification|get_pending")},i.registerActionTypes=async function(i){await d("plugin:notification|register_action_types",{types:i})},i.removeActive=async function(i){await d("plugin:notification|remove_active",{notifications:i})},i.removeAllActive=async function(){await d("plugin:notification|remove_active")},i.removeChannel=async function(i){await d("plugin:notification|delete_channel",{id:i})},i.requestPermission=async function(){return await window.Notification.requestPermission()},i.sendNotification=function(i){"string"==typeof i?new window.Notification(i):new window.Notification(i.title,i)},i}({});Object.defineProperty(window.__TAURI__,"notification",{value:__TAURI_PLUGIN_NOTIFICATION__})}
+12
View File
@@ -1,5 +1,17 @@
# Changelog
## \[2.0.3]
### Dependencies
- Upgraded to `fs@2.0.3`
## \[2.0.2]
### Dependencies
- Upgraded to `fs@2.0.2`
## \[2.0.1]
- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-persisted-scope"
version = "2.0.1"
version = "2.0.3"
description = "Save filesystem and asset scopes and restore them when the app is reopened."
authors = { workspace = true }
license = { workspace = true }
@@ -27,7 +27,7 @@ log = { workspace = true }
thiserror = { workspace = true }
aho-corasick = "1"
bincode = "1"
tauri-plugin-fs = { path = "../fs", version = "2.0.1" }
tauri-plugin-fs = { path = "../fs", version = "2.0.3" }
[features]
protocol-asset = ["tauri/protocol-asset"]
+4
View File
@@ -2,6 +2,10 @@
## \[2.0.1]
- [`3c1f3874`](https://github.com/tauri-apps/plugins-workspace/commit/3c1f3874f4c828637b3aa983cba13c77427faf58) ([#1911](https://github.com/tauri-apps/plugins-workspace/pull/1911) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Added missing permission for `handleIconState` and fixed its event processing logic.
## \[2.0.1]
- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
## \[2.0.0]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-positioner"
version = "2.0.1"
version = "2.0.2"
description = "Position your windows at well-known locations."
authors = { workspace = true }
license = { workspace = true }
+1 -1
View File
@@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_POSITIONER__=function(t){"use strict";async function e(t,e={},o){return window.__TAURI_INTERNALS__.invoke(t,e,o)}var o;return"function"==typeof SuppressedError&&SuppressedError,t.Position=void 0,(o=t.Position||(t.Position={}))[o.TopLeft=0]="TopLeft",o[o.TopRight=1]="TopRight",o[o.BottomLeft=2]="BottomLeft",o[o.BottomRight=3]="BottomRight",o[o.TopCenter=4]="TopCenter",o[o.BottomCenter=5]="BottomCenter",o[o.LeftCenter=6]="LeftCenter",o[o.RightCenter=7]="RightCenter",o[o.Center=8]="Center",o[o.TrayLeft=9]="TrayLeft",o[o.TrayBottomLeft=10]="TrayBottomLeft",o[o.TrayRight=11]="TrayRight",o[o.TrayBottomRight=12]="TrayBottomRight",o[o.TrayCenter=13]="TrayCenter",o[o.TrayBottomCenter=14]="TrayBottomCenter",t.handleIconState=async function(t){const o={};o[`${t.rect.size.type}`]={width:t.rect.size.width,height:t.rect.size.height};const i={};i[`${t.rect.position.type}`]={x:t.rect.position.x,y:t.rect.position.y},await e("plugin:positioner|set_tray_icon_state",{position:i,size:o})},t.moveWindow=async function(t){await e("plugin:positioner|move_window",{position:t})},t}({});Object.defineProperty(window.__TAURI__,"positioner",{value:__TAURI_PLUGIN_POSITIONER__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_POSITIONER__=function(t){"use strict";async function o(t,o={},e){return window.__TAURI_INTERNALS__.invoke(t,o,e)}var e;return"function"==typeof SuppressedError&&SuppressedError,t.Position=void 0,(e=t.Position||(t.Position={}))[e.TopLeft=0]="TopLeft",e[e.TopRight=1]="TopRight",e[e.BottomLeft=2]="BottomLeft",e[e.BottomRight=3]="BottomRight",e[e.TopCenter=4]="TopCenter",e[e.BottomCenter=5]="BottomCenter",e[e.LeftCenter=6]="LeftCenter",e[e.RightCenter=7]="RightCenter",e[e.Center=8]="Center",e[e.TrayLeft=9]="TrayLeft",e[e.TrayBottomLeft=10]="TrayBottomLeft",e[e.TrayRight=11]="TrayRight",e[e.TrayBottomRight=12]="TrayBottomRight",e[e.TrayCenter=13]="TrayCenter",e[e.TrayBottomCenter=14]="TrayBottomCenter",t.handleIconState=async function(t){await o("plugin:positioner|set_tray_icon_state",{position:t.rect.position,size:t.rect.size})},t.moveWindow=async function(t){await o("plugin:positioner|move_window",{position:t})},t}({});Object.defineProperty(window.__TAURI__,"positioner",{value:__TAURI_PLUGIN_POSITIONER__})}
+1 -1
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
const COMMANDS: &[&str] = &["move_window"];
const COMMANDS: &[&str] = &["move_window", "set_tray_icon_state"];
fn main() {
tauri_plugin::Builder::new(COMMANDS)
+2 -14
View File
@@ -40,20 +40,8 @@ export async function moveWindow(to: Position): Promise<void> {
}
export async function handleIconState(event: TrayIconEvent): Promise<void> {
const size = {} as Record<string, unknown>
size[`${event.rect.size.type}`] = {
width: event.rect.size.width,
height: event.rect.size.height
}
const position = {} as Record<string, unknown>
position[`${event.rect.position.type}`] = {
x: event.rect.position.x,
y: event.rect.position.y
}
await invoke('plugin:positioner|set_tray_icon_state', {
position,
size
position: event.rect.position,
size: event.rect.size
})
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-positioner",
"version": "2.0.0",
"version": "2.0.1",
"description": "Position your windows at well-known locations.",
"license": "MIT OR Apache-2.0",
"authors": [
@@ -4,5 +4,10 @@
[[permission]]
identifier = "allow-set-tray-icon-state"
description = "Enables the set_tray_icon_state to handle events and set the TrayIcon state."
description = "Enables the set_tray_icon_state command without any pre-configured scope."
commands.allow = ["set_tray_icon_state"]
[[permission]]
identifier = "deny-set-tray-icon-state"
description = "Denies the set_tray_icon_state command without any pre-configured scope."
commands.deny = ["set_tray_icon_state"]
@@ -1,6 +1,6 @@
## Default Permission
Allows the move_window command
Allows the moveWindow and handleIconState APIs
- `allow-move-window`
- `set-tray-icon-state`
@@ -48,7 +48,20 @@ Denies the move_window command without any pre-configured scope.
</td>
<td>
Enables the set_tray_icon_state to handle events and set the TrayIcon state.
Enables the set_tray_icon_state command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`positioner:deny-set-tray-icon-state`
</td>
<td>
Denies the set_tray_icon_state command without any pre-configured scope.
</td>
</tr>
+1 -1
View File
@@ -1,4 +1,4 @@
"$schema" = "schemas/schema.json"
[default]
description = "Allows the move_window command"
description = "Allows the moveWindow and handleIconState APIs"
permissions = ["allow-move-window", "set-tray-icon-state"]
@@ -305,12 +305,17 @@
"const": "deny-move-window"
},
{
"description": "Enables the set_tray_icon_state to handle events and set the TrayIcon state.",
"description": "Enables the set_tray_icon_state command without any pre-configured scope.",
"type": "string",
"const": "allow-set-tray-icon-state"
},
{
"description": "Allows the move_window command",
"description": "Denies the set_tray_icon_state command without any pre-configured scope.",
"type": "string",
"const": "deny-set-tray-icon-state"
},
{
"description": "Allows the moveWindow and handleIconState APIs",
"type": "string",
"const": "default"
}
+4
View File
@@ -2,6 +2,10 @@
## \[2.0.1]
- [`51ddf6a7`](https://github.com/tauri-apps/plugins-workspace/commit/51ddf6a71544acfb261ffc9393dab1342da0a219) ([#1881](https://github.com/tauri-apps/plugins-workspace/pull/1881) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) On Windows, Fix `open` JS API hanging and freezing the app.
## \[2.0.1]
- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
## \[2.0.0]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-shell"
version = "2.0.1"
version = "2.0.2"
description = "Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application."
edition = { workspace = true }
authors = { workspace = true }
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-shell",
"version": "2.0.0",
"version": "2.0.1",
"license": "MIT OR Apache-2.0",
"authors": [
"Tauri Programme within The Commons Conservancy"
+1 -1
View File
@@ -303,7 +303,7 @@ pub fn kill<R: Runtime>(
}
#[tauri::command]
pub fn open<R: Runtime>(
pub async fn open<R: Runtime>(
_window: Window<R>,
shell: State<'_, Shell<R>>,
path: String,
@@ -9,6 +9,6 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@tauri-apps/cli": "2.0.0"
"@tauri-apps/cli": "2.0.4"
}
}
+4
View File
@@ -2,6 +2,10 @@
## \[2.0.1]
- [`0ca4cc91`](https://github.com/tauri-apps/plugins-workspace/commit/0ca4cc914c5ea995c98f9e60a2ab49827c219350) ([#1963](https://github.com/tauri-apps/plugins-workspace/pull/1963) by [@yoggys](https://github.com/tauri-apps/plugins-workspace/../../yoggys)) Fixed incorrect documentation of the select method in the Database class.
## \[2.0.1]
- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
## \[2.0.0]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-sql"
version = "2.0.1"
version = "2.0.2"
description = "Interface with SQL databases."
authors = { workspace = true }
license = { workspace = true }
+2 -2
View File
@@ -129,12 +129,12 @@ export default class Database {
* ```ts
* // for sqlite & postgres
* const result = await db.select(
* "SELECT * from todos WHERE id = $1", id
* "SELECT * from todos WHERE id = $1", [ id ]
* );
*
* // for mysql
* const result = await db.select(
* "SELECT * from todos WHERE id = ?", id
* "SELECT * from todos WHERE id = ?", [ id ]
* );
* ```
*/
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-sql",
"version": "2.0.0",
"version": "2.0.1",
"description": "Interface with SQL databases",
"license": "MIT OR Apache-2.0",
"authors": [
+7 -9
View File
@@ -23,7 +23,7 @@ pub(crate) async fn load<R: Runtime>(
pool.migrate(&migrator).await?;
}
db_instances.0.lock().await.insert(db.clone(), pool);
db_instances.0.write().await.insert(db.clone(), pool);
Ok(db)
}
@@ -36,7 +36,7 @@ pub(crate) async fn close(
db_instances: State<'_, DbInstances>,
db: Option<String>,
) -> Result<bool, crate::Error> {
let mut instances = db_instances.0.lock().await;
let instances = db_instances.0.read().await;
let pools = if let Some(db) = db {
vec![db]
@@ -45,9 +45,7 @@ pub(crate) async fn close(
};
for pool in pools {
let db = instances
.get_mut(&pool)
.ok_or(Error::DatabaseNotLoaded(pool))?;
let db = instances.get(&pool).ok_or(Error::DatabaseNotLoaded(pool))?;
db.close().await;
}
@@ -62,9 +60,9 @@ pub(crate) async fn execute(
query: String,
values: Vec<JsonValue>,
) -> Result<(u64, LastInsertId), crate::Error> {
let mut instances = db_instances.0.lock().await;
let instances = db_instances.0.read().await;
let db = instances.get_mut(&db).ok_or(Error::DatabaseNotLoaded(db))?;
let db = instances.get(&db).ok_or(Error::DatabaseNotLoaded(db))?;
db.execute(query, values).await
}
@@ -75,8 +73,8 @@ pub(crate) async fn select(
query: String,
values: Vec<JsonValue>,
) -> Result<Vec<IndexMap<String, JsonValue>>, crate::Error> {
let mut instances = db_instances.0.lock().await;
let instances = db_instances.0.read().await;
let db = instances.get_mut(&db).ok_or(Error::DatabaseNotLoaded(db))?;
let db = instances.get(&db).ok_or(Error::DatabaseNotLoaded(db))?;
db.select(query, values).await
}
+7 -5
View File
@@ -29,12 +29,12 @@ use tauri::{
plugin::{Builder as PluginBuilder, TauriPlugin},
Manager, RunEvent, Runtime,
};
use tokio::sync::Mutex;
use tokio::sync::{Mutex, RwLock};
use std::collections::HashMap;
#[derive(Default)]
pub struct DbInstances(pub Mutex<HashMap<String, DbPool>>);
pub struct DbInstances(pub RwLock<HashMap<String, DbPool>>);
#[derive(Serialize)]
#[serde(untagged)]
@@ -140,12 +140,14 @@ impl Builder {
tauri::async_runtime::block_on(async move {
let instances = DbInstances::default();
let mut lock = instances.0.lock().await;
let mut lock = instances.0.write().await;
for db in config.preload {
let pool = DbPool::connect(&db, app).await?;
if let Some(migrations) = self.migrations.as_mut().unwrap().remove(&db) {
if let Some(migrations) =
self.migrations.as_mut().and_then(|mm| mm.remove(&db))
{
let migrator = Migrator::new(migrations).await?;
pool.migrate(&migrator).await?;
}
@@ -166,7 +168,7 @@ impl Builder {
if let RunEvent::Exit = event {
tauri::async_runtime::block_on(async move {
let instances = &*app.state::<DbInstances>();
let instances = instances.0.lock().await;
let instances = instances.0.read().await;
for value in instances.values() {
value.close().await;
}
+16
View File
@@ -1,5 +1,13 @@
# Changelog
## \[2.1.0]
### feat
- [`8c67d44a`](https://github.com/tauri-apps/plugins-workspace/commit/8c67d44aef60b1427019538d8420787ef35bd3d5) ([#1860](https://github.com/tauri-apps/plugins-workspace/pull/1860) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) - Add `getStore`
- Add an option to use pre-stored (de)serialize functions (registered on rust)
- Add `LazyStore`
## \[2.0.1]
- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
@@ -125,3 +133,11 @@
com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
plugins-workspace/pull/371)) First v2 alpha release!
com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
) First v2 alpha release!
com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
eb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
ps://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
plugins-workspace/pull/371)) First v2 alpha release!
com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-store"
version = "2.0.1"
version = "2.1.0"
description = "Simple, persistent key-value store."
authors = { workspace = true }
license = { workspace = true }
+17 -42
View File
@@ -70,7 +70,7 @@ Afterwards all the plugin's APIs are available through the JavaScript guest bind
```typescript
import { Store } from '@tauri-apps/plugin-store'
const store = new Store('.settings.dat')
const store = await Store.load('settings.json')
await store.set('some-key', { value: 5 })
@@ -81,14 +81,11 @@ if (val) {
} else {
console.log('val is null')
}
// This manually saves the store.
await store.save()
```
### Persisting Values
As seen above, values added to the store are not persisted between application loads unless the application is closed gracefully.
Modifications made to the store are automatically saved by default
You can manually save a store with:
@@ -103,65 +100,43 @@ However, you can also load them manually later like so:
await store.load()
```
### LazyStore
There's also a high level API `LazyStore` which only loads the store on first access, note that the options will be ignored if a `Store` with that path has already been created
```typescript
import { LazyStore } from '@tauri-apps/plugin-store'
const store = new LazyStore('settings.json')
```
## Usage from Rust
You can also create `Store` instances directly in Rust:
```rust
use tauri_plugin_store::StoreBuilder;
use tauri_plugin_store::StoreExt;
use serde_json::json;
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_store::Builder::default().build())
.setup(|app| {
let mut store = StoreBuilder::new("app_data.bin").build(app.handle().clone());
// Attempt to load the store, if it's saved already.
store.load().expect("Failed to load store from disk");
// This loads the store from disk
let store = app.store("app_data.json")?;
// Note that values must be serde_json::Value instances,
// otherwise, they will not be compatible with the JavaScript bindings.
store.insert("a".to_string(), json!("b"));
// You can manually save the store after making changes.
// Otherwise, it will save upon graceful exit as described above.
store.save()
store.set("a".to_string(), json!("b"));
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
```
### Loading Gracefully
If you call `load` on a `Store` that hasn't yet been written to the disk, it will return an error. You must handle this error if you want to gracefully continue and use the default store until you save it to the disk. The example above shows how to do this.
For example, this would cause a panic if the store has not yet been created:
```rust
store.load().unwrap();
```
Rather than silently continuing like you may expect.
You should always handle the error appropriately rather than unwrapping, or you may experience unexpected app crashes:
```rust
store.load().expect("Failed to load store from disk");
```
### Frontend Interoperability
As you may have noticed, the `Store` crated above isn't accessible to the frontend. To interoperate with stores created by JavaScript use the exported `with_store` method:
```rust
use tauri::Wry;
use tauri_plugin_store::StoreExt;
let store = app.store_builder("app_data.bin").build();
store.insert("key", "value");
```
The store created from both Rust side and JavaScript side are stored in the app's resource table and can be accessed by both sides, you can access it by using the same path, with `getStore` and `LazyStore` in the JavaScript side and `get_store` and `store` in the Rust side
## Contributing
+1 -1
View File
@@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_STORE__=function(e){"use strict";var t,r;function a(e,t=!1){return window.__TAURI_INTERNALS__.transformCallback(e,t)}async function i(e,t={},r){return window.__TAURI_INTERNALS__.invoke(e,t,r)}"function"==typeof SuppressedError&&SuppressedError;class n{get rid(){return function(e,t,r,a){if("a"===r&&!a)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!a:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?a:"a"===r?a.call(e):a?a.value:t.get(e)}(this,t,"f")}constructor(e){t.set(this,void 0),function(e,t,r,a,i){if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");t.set(e,r)}(this,t,e)}async close(){return i("plugin:resources|close",{rid:this.rid})}}async function s(e,t,r){const n={kind:"Any"};return i("plugin:event|listen",{event:e,target:n,handler:a(t)}).then((t=>async()=>async function(e,t){await i("plugin:event|unlisten",{event:e,eventId:t})}(e,t)))}t=new WeakMap,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"}(r||(r={}));class o extends n{constructor(e,t){super(e),this.path=t}async set(e,t){await i("plugin:store|set",{rid:this.rid,key:e,value:t})}async get(e){return await i("plugin:store|get",{rid:this.rid,key:e})}async has(e){return await i("plugin:store|has",{rid:this.rid,key:e})}async delete(e){return await i("plugin:store|delete",{rid:this.rid,key:e})}async clear(){await i("plugin:store|clear",{rid:this.rid})}async reset(){await i("plugin:store|reset",{rid:this.rid})}async keys(){return await i("plugin:store|keys",{rid:this.rid})}async values(){return await i("plugin:store|values",{rid:this.rid})}async entries(){return await i("plugin:store|entries",{rid:this.rid})}async length(){return await i("plugin:store|length",{rid:this.rid})}async load(){await i("plugin:store|load",{rid:this.rid})}async save(){await i("plugin:store|save",{rid:this.rid})}async onKeyChange(e,t){return await s("store://change",(r=>{r.payload.path===this.path&&r.payload.key===e&&t(r.payload.value)}))}async onChange(e){return await s("store://change",(t=>{t.payload.path===this.path&&e(t.payload.key,t.payload.value)}))}}return e.Store=o,e.createStore=async function(e,t){const r=await i("plugin:store|create_store",{path:e,...t});return new o(r,e)},e}({});Object.defineProperty(window.__TAURI__,"store",{value:__TAURI_PLUGIN_STORE__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_STORE__=function(t){"use strict";var e,a;function r(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}async function s(t,e={},a){return window.__TAURI_INTERNALS__.invoke(t,e,a)}"function"==typeof SuppressedError&&SuppressedError;class i{get rid(){return function(t,e,a,r){if("a"===a&&!r)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof e?t!==e||!r:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===a?r:"a"===a?r.call(t):r?r.value:e.get(t)}(this,e,"f")}constructor(t){e.set(this,void 0),function(t,e,a,r,s){if("function"==typeof e?t!==e||!s:!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");e.set(t,a)}(this,e,t)}async close(){return s("plugin:resources|close",{rid:this.rid})}}async function n(t,e,a){const i={kind:"Any"};return s("plugin:event|listen",{event:t,target:i,handler:r(e)}).then((e=>async()=>async function(t,e){await s("plugin:event|unlisten",{event:t,eventId:e})}(t,e)))}async function o(t,e){return await u.load(t,e)}e=new WeakMap,function(t){t.WINDOW_RESIZED="tauri://resize",t.WINDOW_MOVED="tauri://move",t.WINDOW_CLOSE_REQUESTED="tauri://close-requested",t.WINDOW_DESTROYED="tauri://destroyed",t.WINDOW_FOCUS="tauri://focus",t.WINDOW_BLUR="tauri://blur",t.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",t.WINDOW_THEME_CHANGED="tauri://theme-changed",t.WINDOW_CREATED="tauri://window-created",t.WEBVIEW_CREATED="tauri://webview-created",t.DRAG_ENTER="tauri://drag-enter",t.DRAG_OVER="tauri://drag-over",t.DRAG_DROP="tauri://drag-drop",t.DRAG_LEAVE="tauri://drag-leave"}(a||(a={}));class u extends i{constructor(t){super(t)}static async load(t,e){const a=await s("plugin:store|load",{path:t,...e});return new u(a)}static async get(t){return await s("plugin:store|get_store",{path:t}).then((t=>t?new u(t):null))}async set(t,e){await s("plugin:store|set",{rid:this.rid,key:t,value:e})}async get(t){const[e,a]=await s("plugin:store|get",{rid:this.rid,key:t});return a?e:void 0}async has(t){return await s("plugin:store|has",{rid:this.rid,key:t})}async delete(t){return await s("plugin:store|delete",{rid:this.rid,key:t})}async clear(){await s("plugin:store|clear",{rid:this.rid})}async reset(){await s("plugin:store|reset",{rid:this.rid})}async keys(){return await s("plugin:store|keys",{rid:this.rid})}async values(){return await s("plugin:store|values",{rid:this.rid})}async entries(){return await s("plugin:store|entries",{rid:this.rid})}async length(){return await s("plugin:store|length",{rid:this.rid})}async reload(){await s("plugin:store|reload",{rid:this.rid})}async save(){await s("plugin:store|save",{rid:this.rid})}async onKeyChange(t,e){return await n("store://change",(a=>{a.payload.resourceId===this.rid&&a.payload.key===t&&e(a.payload.exists?a.payload.value:void 0)}))}async onChange(t){return await n("store://change",(e=>{e.payload.resourceId===this.rid&&t(e.payload.key,e.payload.exists?e.payload.value:void 0)}))}}return t.LazyStore=class{get store(){return this._store||(this._store=o(this.path,this.options)),this._store}constructor(t,e){this.path=t,this.options=e}async init(){await this.store}async set(t,e){return(await this.store).set(t,e)}async get(t){return(await this.store).get(t)}async has(t){return(await this.store).has(t)}async delete(t){return(await this.store).delete(t)}async clear(){await(await this.store).clear()}async reset(){await(await this.store).reset()}async keys(){return(await this.store).keys()}async values(){return(await this.store).values()}async entries(){return(await this.store).entries()}async length(){return(await this.store).length()}async reload(){await(await this.store).reload()}async save(){await(await this.store).save()}async onKeyChange(t,e){return(await this.store).onKeyChange(t,e)}async onChange(t){return(await this.store).onChange(t)}async close(){this._store&&await(await this._store).close()}},t.Store=u,t.getStore=async function(t){return await u.get(t)},t.load=o,t}({});Object.defineProperty(window.__TAURI__,"store",{value:__TAURI_PLUGIN_STORE__})}
+4 -3
View File
@@ -3,7 +3,8 @@
// SPDX-License-Identifier: MIT
const COMMANDS: &[&str] = &[
"create_store",
"load",
"get_store",
"set",
"get",
"has",
@@ -12,9 +13,9 @@ const COMMANDS: &[&str] = &[
"reset",
"keys",
"values",
"length",
"entries",
"load",
"length",
"reload",
"save",
];
@@ -8,7 +8,7 @@
"tauri": "tauri"
},
"devDependencies": {
"@tauri-apps/cli": "2.0.0",
"@tauri-apps/cli": "2.0.4",
"vite": "^5.0.12",
"typescript": "^5.4.7"
}
@@ -5,9 +5,8 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::time::Duration;
use serde_json::json;
use tauri::Listener;
use tauri_plugin_store::StoreExt;
mod app;
@@ -18,17 +17,11 @@ fn main() {
.plugin(tauri_plugin_store::Builder::new().build())
.setup(|app| {
// Init store and load it from disk
let store = app
.handle()
.store_builder("settings.json")
.auto_save(Duration::from_millis(100))
.build();
// If there are no saved settings yet, this will return an error so we ignore the return value.
let _ = store.load();
let store = app.store("settings.json")?;
app.listen("store://change", |event| {
dbg!(event);
});
let app_settings = AppSettings::load_from_store(&store);
match app_settings {
Ok(app_settings) => {
let theme = app_settings.theme;
@@ -3,6 +3,7 @@
"version": "0.1.0",
"identifier": "com.tauri.app-settings-manager",
"build": {
"devUrl": "http://localhost:1420",
"frontendDist": "../dist"
},
"app": {
+317 -94
View File
@@ -8,8 +8,10 @@ import { invoke, Resource } from '@tauri-apps/api/core'
interface ChangePayload<T> {
path: string
resourceId?: number
key: string
value: T | null
value: T
exists: boolean
}
/**
@@ -17,41 +19,213 @@ interface ChangePayload<T> {
*/
export type StoreOptions = {
/**
* Auto save on modification with debounce duration in milliseconds
* Auto save on modification with debounce duration in milliseconds, it's 100ms by default, pass in `false` to disable it
*/
autoSave?: boolean
autoSave?: boolean | number
/**
* Name of a serialize function registered in the rust side plugin builder
*/
serializeFnName?: string
/**
* Name of a deserialize function registered in the rust side plugin builder
*/
deserializeFnName?: string
/**
* Force create a new store with default values even if it already exists.
*/
createNew?: boolean
}
/**
* @param path: Path to save the store in `app_data_dir`
* @param options: Store configuration options
* Create a new Store or load the existing store with the path.
*
* @example
* ```typescript
* import { Store } from '@tauri-apps/api/store';
* const store = await Store.load('store.json');
* ```
*
* @param path Path to save the store in `app_data_dir`
* @param options Store configuration options
*/
export async function createStore(path: string, options?: StoreOptions) {
const resourceId = await invoke<number>('plugin:store|create_store', {
path,
...options
})
return new Store(resourceId, path)
export async function load(
path: string,
options?: StoreOptions
): Promise<Store> {
return await Store.load(path, options)
}
/**
* Gets an already loaded store.
*
* If the store is not loaded, returns `null`. In this case you must {@link Store.load load} it.
*
* This function is more useful when you already know the store is loaded
* and just need to access its instance. Prefer {@link Store.load} otherwise.
*
* @example
* ```typescript
* import { getStore } from '@tauri-apps/api/store';
* const store = await getStore('store.json');
* ```
*
* @param path Path of the store.
*/
export async function getStore(path: string): Promise<Store | null> {
return await Store.get(path)
}
/**
* A lazy loaded key-value store persisted by the backend layer.
*/
export class Store extends Resource {
export class LazyStore implements IStore {
private _store?: Promise<Store>
private get store(): Promise<Store> {
if (!this._store) {
this._store = load(this.path, this.options)
}
return this._store
}
/**
* Note that the options are not applied if someone else already created the store
* @param path Path to save the store in `app_data_dir`
* @param options Store configuration options
*/
constructor(
rid: number,
private readonly path: string
) {
private readonly path: string,
private readonly options?: StoreOptions
) {}
/**
* Init/load the store if it's not loaded already
*/
async init(): Promise<void> {
await this.store
}
async set(key: string, value: unknown): Promise<void> {
return (await this.store).set(key, value)
}
async get<T>(key: string): Promise<T | undefined> {
return (await this.store).get<T>(key)
}
async has(key: string): Promise<boolean> {
return (await this.store).has(key)
}
async delete(key: string): Promise<boolean> {
return (await this.store).delete(key)
}
async clear(): Promise<void> {
await (await this.store).clear()
}
async reset(): Promise<void> {
await (await this.store).reset()
}
async keys(): Promise<string[]> {
return (await this.store).keys()
}
async values<T>(): Promise<T[]> {
return (await this.store).values<T>()
}
async entries<T>(): Promise<Array<[key: string, value: T]>> {
return (await this.store).entries<T>()
}
async length(): Promise<number> {
return (await this.store).length()
}
async reload(): Promise<void> {
await (await this.store).reload()
}
async save(): Promise<void> {
await (await this.store).save()
}
async onKeyChange<T>(
key: string,
cb: (value: T | undefined) => void
): Promise<UnlistenFn> {
return (await this.store).onKeyChange<T>(key, cb)
}
async onChange<T>(
cb: (key: string, value: T | undefined) => void
): Promise<UnlistenFn> {
return (await this.store).onChange<T>(cb)
}
async close(): Promise<void> {
if (this._store) {
await (await this._store).close()
}
}
}
/**
* A key-value store persisted by the backend layer.
*/
export class Store extends Resource implements IStore {
private constructor(rid: number) {
super(rid)
}
/**
* Inserts a key-value pair into the store.
* Create a new Store or load the existing store with the path.
*
* @param key
* @param value
* @returns
* @example
* ```typescript
* import { Store } from '@tauri-apps/api/store';
* const store = await Store.load('store.json');
* ```
*
* @param path Path to save the store in `app_data_dir`
* @param options Store configuration options
*/
static async load(path: string, options?: StoreOptions): Promise<Store> {
const rid = await invoke<number>('plugin:store|load', {
path,
...options
})
return new Store(rid)
}
/**
* Gets an already loaded store.
*
* If the store is not loaded, returns `null`. In this case you must {@link Store.load load} it.
*
* This function is more useful when you already know the store is loaded
* and just need to access its instance. Prefer {@link Store.load} otherwise.
*
* @example
* ```typescript
* import { Store } from '@tauri-apps/api/store';
* let store = await Store.get('store.json');
* if (!store) {
* store = await Store.load('store.json');
* }
* ```
*
* @param path Path of the store.
*/
static async get(path: string): Promise<Store | null> {
return await invoke<number | null>('plugin:store|get_store', { path }).then(
(rid) => (rid ? new Store(rid) : null)
)
}
async set(key: string, value: unknown): Promise<void> {
await invoke('plugin:store|set', {
rid: this.rid,
@@ -60,25 +234,14 @@ export class Store extends Resource {
})
}
/**
* Returns the value for the given `key` or `null` the key does not exist.
*
* @param key
* @returns
*/
async get<T>(key: string): Promise<T | null> {
return await invoke('plugin:store|get', {
async get<T>(key: string): Promise<T | undefined> {
const [value, exists] = await invoke<[T, boolean]>('plugin:store|get', {
rid: this.rid,
key
})
return exists ? value : undefined
}
/**
* Returns `true` if the given `key` exists in the store.
*
* @param key
* @returns
*/
async has(key: string): Promise<boolean> {
return await invoke('plugin:store|has', {
rid: this.rid,
@@ -86,12 +249,6 @@ export class Store extends Resource {
})
}
/**
* Removes a key-value pair from the store.
*
* @param key
* @returns
*/
async delete(key: string): Promise<boolean> {
return await invoke('plugin:store|delete', {
rid: this.rid,
@@ -99,84 +256,156 @@ export class Store extends Resource {
})
}
/**
* Clears the store, removing all key-value pairs.
*
* Note: To clear the storage and reset it to it's `default` value, use `reset` instead.
* @returns
*/
async clear(): Promise<void> {
await invoke('plugin:store|clear', { rid: this.rid })
}
/**
* Resets the store to it's `default` value.
*
* If no default value has been set, this method behaves identical to `clear`.
* @returns
*/
async reset(): Promise<void> {
await invoke('plugin:store|reset', { rid: this.rid })
}
/**
* Returns a list of all key in the store.
*
* @returns
*/
async keys(): Promise<string[]> {
return await invoke('plugin:store|keys', { rid: this.rid })
}
async values<T>(): Promise<T[]> {
return await invoke('plugin:store|values', { rid: this.rid })
}
async entries<T>(): Promise<Array<[key: string, value: T]>> {
return await invoke('plugin:store|entries', { rid: this.rid })
}
async length(): Promise<number> {
return await invoke('plugin:store|length', { rid: this.rid })
}
async reload(): Promise<void> {
await invoke('plugin:store|reload', { rid: this.rid })
}
async save(): Promise<void> {
await invoke('plugin:store|save', { rid: this.rid })
}
async onKeyChange<T>(
key: string,
cb: (value: T | undefined) => void
): Promise<UnlistenFn> {
return await listen<ChangePayload<T>>('store://change', (event) => {
if (event.payload.resourceId === this.rid && event.payload.key === key) {
cb(event.payload.exists ? event.payload.value : undefined)
}
})
}
async onChange<T>(
cb: (key: string, value: T | undefined) => void
): Promise<UnlistenFn> {
return await listen<ChangePayload<T>>('store://change', (event) => {
if (event.payload.resourceId === this.rid) {
cb(
event.payload.key,
event.payload.exists ? event.payload.value : undefined
)
}
})
}
}
interface IStore {
/**
* Inserts a key-value pair into the store.
*
* @param key
* @param value
* @returns
*/
set(key: string, value: unknown): Promise<void>
/**
* Returns the value for the given `key` or `undefined` if the key does not exist.
*
* @param key
* @returns
*/
get<T>(key: string): Promise<T | undefined>
/**
* Returns `true` if the given `key` exists in the store.
*
* @param key
* @returns
*/
has(key: string): Promise<boolean>
/**
* Removes a key-value pair from the store.
*
* @param key
* @returns
*/
delete(key: string): Promise<boolean>
/**
* Clears the store, removing all key-value pairs.
*
* Note: To clear the storage and reset it to its `default` value, use {@linkcode reset} instead.
* @returns
*/
clear(): Promise<void>
/**
* Resets the store to its `default` value.
*
* If no default value has been set, this method behaves identical to {@linkcode clear}.
* @returns
*/
reset(): Promise<void>
/**
* Returns a list of all keys in the store.
*
* @returns
*/
keys(): Promise<string[]>
/**
* Returns a list of all values in the store.
*
* @returns
*/
async values<T>(): Promise<T[]> {
return await invoke('plugin:store|values', { rid: this.rid })
}
values<T>(): Promise<T[]>
/**
* Returns a list of all entries in the store.
*
* @returns
*/
async entries<T>(): Promise<Array<[key: string, value: T]>> {
return await invoke('plugin:store|entries', { rid: this.rid })
}
entries<T>(): Promise<Array<[key: string, value: T]>>
/**
* Returns the number of key-value pairs in the store.
*
* @returns
*/
async length(): Promise<number> {
return await invoke('plugin:store|length', { rid: this.rid })
}
length(): Promise<number>
/**
* Attempts to load the on-disk state at the stores `path` into memory.
* Attempts to load the on-disk state at the store's `path` into memory.
*
* This method is useful if the on-disk state was edited by the user and you want to synchronize the changes.
*
* Note: This method does not emit change events.
* @returns
*/
async load(): Promise<void> {
await invoke('plugin:store|load', { rid: this.rid })
}
reload(): Promise<void>
/**
* Saves the store to disk at the stores `path`.
*
* As the store is only persisted to disk before the apps exit, changes might be lost in a crash.
* This method lets you persist the store to disk whenever you deem necessary.
* Saves the store to disk at the store's `path`.
* @returns
*/
async save(): Promise<void> {
await invoke('plugin:store|save', { rid: this.rid })
}
save(): Promise<void>
/**
* Listen to changes on a store key.
@@ -186,16 +415,10 @@ export class Store extends Resource {
*
* @since 2.0.0
*/
async onKeyChange<T>(
onKeyChange<T>(
key: string,
cb: (value: T | null) => void
): Promise<UnlistenFn> {
return await listen<ChangePayload<T>>('store://change', (event) => {
if (event.payload.path === this.path && event.payload.key === key) {
cb(event.payload.value)
}
})
}
cb: (value: T | undefined) => void
): Promise<UnlistenFn>
/**
* Listen to changes on the store.
@@ -204,13 +427,13 @@ export class Store extends Resource {
*
* @since 2.0.0
*/
async onChange<T>(
cb: (key: string, value: T | null) => void
): Promise<UnlistenFn> {
return await listen<ChangePayload<T>>('store://change', (event) => {
if (event.payload.path === this.path) {
cb(event.payload.key, event.payload.value)
}
})
}
onChange<T>(
cb: (key: string, value: T | undefined) => void
): Promise<UnlistenFn>
/**
* Close the store and cleans up this resource from memory.
* **You should not call any method on this object anymore and should drop any reference to it.**
*/
close(): Promise<void>
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-store",
"version": "2.0.0",
"version": "2.1.0",
"description": "Simple, persistent key-value store.",
"license": "MIT OR Apache-2.0",
"authors": [
@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-create-store"
description = "Enables the create_store command without any pre-configured scope."
commands.allow = ["create_store"]
[[permission]]
identifier = "deny-create-store"
description = "Denies the create_store command without any pre-configured scope."
commands.deny = ["create_store"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-get-store"
description = "Enables the get_store command without any pre-configured scope."
commands.allow = ["get_store"]
[[permission]]
identifier = "deny-get-store"
description = "Denies the get_store command without any pre-configured scope."
commands.deny = ["get_store"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-reload"
description = "Enables the reload command without any pre-configured scope."
commands.allow = ["reload"]
[[permission]]
identifier = "deny-reload"
description = "Denies the reload command without any pre-configured scope."
commands.deny = ["reload"]
@@ -9,19 +9,20 @@ All operations are enabled by default.
- `allow-create-store`
- `allow-clear`
- `allow-delete`
- `allow-entries`
- `allow-load`
- `allow-get-store`
- `allow-set`
- `allow-get`
- `allow-has`
- `allow-keys`
- `allow-length`
- `allow-load`
- `allow-delete`
- `allow-clear`
- `allow-reset`
- `allow-save`
- `allow-set`
- `allow-keys`
- `allow-values`
- `allow-entries`
- `allow-length`
- `allow-reload`
- `allow-save`
## Permission Table
@@ -61,32 +62,6 @@ Denies the clear command without any pre-configured scope.
<tr>
<td>
`store:allow-create-store`
</td>
<td>
Enables the create_store command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`store:deny-create-store`
</td>
<td>
Denies the create_store command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`store:allow-delete`
</td>
@@ -165,6 +140,32 @@ Denies the get command without any pre-configured scope.
<tr>
<td>
`store:allow-get-store`
</td>
<td>
Enables the get_store command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`store:deny-get-store`
</td>
<td>
Denies the get_store command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`store:allow-has`
</td>
@@ -269,6 +270,32 @@ Denies the load command without any pre-configured scope.
<tr>
<td>
`store:allow-reload`
</td>
<td>
Enables the reload command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`store:deny-reload`
</td>
<td>
Denies the reload command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`store:allow-reset`
</td>
+10 -9
View File
@@ -11,17 +11,18 @@ All operations are enabled by default.
"""
permissions = [
"allow-create-store",
"allow-clear",
"allow-delete",
"allow-entries",
"allow-load",
"allow-get-store",
"allow-set",
"allow-get",
"allow-has",
"allow-keys",
"allow-length",
"allow-load",
"allow-delete",
"allow-clear",
"allow-reset",
"allow-save",
"allow-set",
"allow-keys",
"allow-values",
"allow-entries",
"allow-length",
"allow-reload",
"allow-save",
]
+20 -10
View File
@@ -304,16 +304,6 @@
"type": "string",
"const": "deny-clear"
},
{
"description": "Enables the create_store command without any pre-configured scope.",
"type": "string",
"const": "allow-create-store"
},
{
"description": "Denies the create_store command without any pre-configured scope.",
"type": "string",
"const": "deny-create-store"
},
{
"description": "Enables the delete command without any pre-configured scope.",
"type": "string",
@@ -344,6 +334,16 @@
"type": "string",
"const": "deny-get"
},
{
"description": "Enables the get_store command without any pre-configured scope.",
"type": "string",
"const": "allow-get-store"
},
{
"description": "Denies the get_store command without any pre-configured scope.",
"type": "string",
"const": "deny-get-store"
},
{
"description": "Enables the has command without any pre-configured scope.",
"type": "string",
@@ -384,6 +384,16 @@
"type": "string",
"const": "deny-load"
},
{
"description": "Enables the reload command without any pre-configured scope.",
"type": "string",
"const": "allow-reload"
},
{
"description": "Denies the reload command without any pre-configured scope.",
"type": "string",
"const": "deny-reload"
},
{
"description": "Enables the reset command without any pre-configured scope.",
"type": "string",
+9 -4
View File
@@ -3,7 +3,6 @@
// SPDX-License-Identifier: MIT
use serde::{Serialize, Serializer};
use std::path::PathBuf;
pub type Result<T> = std::result::Result<T, Error>;
@@ -21,9 +20,15 @@ pub enum Error {
/// IO error.
#[error(transparent)]
Io(#[from] std::io::Error),
/// Store not found
#[error("Store \"{0}\" not found")]
NotFound(PathBuf),
// /// Store already exists
// #[error("Store at \"{0}\" already exists")]
// AlreadyExists(PathBuf),
/// Serialize function not found
#[error("Serialize Function \"{0}\" not found")]
SerializeFunctionNotFound(String),
/// Deserialize function not found
#[error("Deserialize Function \"{0}\" not found")]
DeserializeFunctionNotFound(String),
/// Some Tauri API failed
#[error(transparent)]
Tauri(#[from] tauri::Error),
+292 -153
View File
@@ -12,234 +12,392 @@
)]
pub use error::{Error, Result};
use log::warn;
use serde::Serialize;
use serde::{Deserialize, Serialize};
pub use serde_json::Value as JsonValue;
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::{Mutex, Weak},
sync::{Arc, Mutex},
time::Duration,
};
pub use store::{Store, StoreBuilder, StoreInner};
pub use store::{resolve_store_path, DeserializeFn, SerializeFn, Store, StoreBuilder};
use tauri::{
plugin::{self, TauriPlugin},
AppHandle, Manager, ResourceId, RunEvent, Runtime, Webview,
AppHandle, Manager, ResourceId, RunEvent, Runtime, State,
};
mod error;
mod store;
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
struct ChangePayload<'a> {
path: &'a Path,
resource_id: Option<u32>,
key: &'a str,
value: &'a JsonValue,
value: Option<&'a JsonValue>,
exists: bool,
}
pub struct StoreCollection<R: Runtime> {
stores: Mutex<HashMap<PathBuf, Weak<Mutex<StoreInner<R>>>>>,
// frozen: bool,
#[derive(Debug)]
struct StoreState {
stores: Arc<Mutex<HashMap<PathBuf, ResourceId>>>,
serialize_fns: HashMap<String, SerializeFn>,
deserialize_fns: HashMap<String, DeserializeFn>,
default_serialize: SerializeFn,
default_deserialize: DeserializeFn,
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum AutoSave {
DebounceDuration(u64),
Bool(bool),
}
fn builder<R: Runtime>(
app: AppHandle<R>,
store_state: State<'_, StoreState>,
path: PathBuf,
auto_save: Option<AutoSave>,
serialize_fn_name: Option<String>,
deserialize_fn_name: Option<String>,
create_new: bool,
) -> Result<StoreBuilder<R>> {
let mut builder = app.store_builder(path);
if let Some(auto_save) = auto_save {
match auto_save {
AutoSave::DebounceDuration(duration) => {
builder = builder.auto_save(Duration::from_millis(duration));
}
AutoSave::Bool(false) => {
builder = builder.disable_auto_save();
}
_ => {}
}
}
if let Some(serialize_fn_name) = serialize_fn_name {
let serialize_fn = store_state
.serialize_fns
.get(&serialize_fn_name)
.ok_or_else(|| crate::Error::SerializeFunctionNotFound(serialize_fn_name))?;
builder = builder.serialize(*serialize_fn);
}
if let Some(deserialize_fn_name) = deserialize_fn_name {
let deserialize_fn = store_state
.deserialize_fns
.get(&deserialize_fn_name)
.ok_or_else(|| crate::Error::DeserializeFunctionNotFound(deserialize_fn_name))?;
builder = builder.deserialize(*deserialize_fn);
}
if create_new {
builder = builder.create_new();
}
Ok(builder)
}
#[tauri::command]
async fn create_store<R: Runtime>(
async fn load<R: Runtime>(
app: AppHandle<R>,
webview: Webview<R>,
store_state: State<'_, StoreState>,
path: PathBuf,
auto_save: Option<u64>,
auto_save: Option<AutoSave>,
serialize_fn_name: Option<String>,
deserialize_fn_name: Option<String>,
create_new: Option<bool>,
) -> Result<ResourceId> {
let mut builder = app.store_builder(path);
if let Some(auto_save) = auto_save {
builder = builder.auto_save(Duration::from_millis(auto_save));
}
let store = builder.build();
Ok(webview.resources_table().add(store))
let builder = builder(
app,
store_state,
path,
auto_save,
serialize_fn_name,
deserialize_fn_name,
create_new.unwrap_or_default(),
)?;
let (_, rid) = builder.build_inner()?;
Ok(rid)
}
#[tauri::command]
async fn get_store<R: Runtime>(
app: AppHandle<R>,
store_state: State<'_, StoreState>,
path: PathBuf,
) -> Result<Option<ResourceId>> {
let stores = store_state.stores.lock().unwrap();
Ok(stores.get(&resolve_store_path(&app, path)?).copied())
}
#[tauri::command]
async fn set<R: Runtime>(
webview: Webview<R>,
app: AppHandle<R>,
rid: ResourceId,
key: String,
value: JsonValue,
) -> Result<()> {
let store = webview.resources_table().get::<Store<R>>(rid)?;
let store = app.resources_table().get::<Store<R>>(rid)?;
store.set(key, value);
Ok(())
}
#[tauri::command]
async fn get<R: Runtime>(
webview: Webview<R>,
app: AppHandle<R>,
rid: ResourceId,
key: String,
) -> Result<Option<JsonValue>> {
let store = webview.resources_table().get::<Store<R>>(rid)?;
Ok(store.get(key))
) -> Result<(Option<JsonValue>, bool)> {
let store = app.resources_table().get::<Store<R>>(rid)?;
let value = store.get(key);
let exists = value.is_some();
Ok((value, exists))
}
#[tauri::command]
async fn has<R: Runtime>(webview: Webview<R>, rid: ResourceId, key: String) -> Result<bool> {
let store = webview.resources_table().get::<Store<R>>(rid)?;
async fn has<R: Runtime>(app: AppHandle<R>, rid: ResourceId, key: String) -> Result<bool> {
let store = app.resources_table().get::<Store<R>>(rid)?;
Ok(store.has(key))
}
#[tauri::command]
async fn delete<R: Runtime>(webview: Webview<R>, rid: ResourceId, key: String) -> Result<bool> {
let store = webview.resources_table().get::<Store<R>>(rid)?;
async fn delete<R: Runtime>(app: AppHandle<R>, rid: ResourceId, key: String) -> Result<bool> {
let store = app.resources_table().get::<Store<R>>(rid)?;
Ok(store.delete(key))
}
#[tauri::command]
async fn clear<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> Result<()> {
let store = webview.resources_table().get::<Store<R>>(rid)?;
async fn clear<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> Result<()> {
let store = app.resources_table().get::<Store<R>>(rid)?;
store.clear();
Ok(())
}
#[tauri::command]
async fn reset<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> Result<()> {
let store = webview.resources_table().get::<Store<R>>(rid)?;
async fn reset<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> Result<()> {
let store = app.resources_table().get::<Store<R>>(rid)?;
store.reset();
Ok(())
}
#[tauri::command]
async fn keys<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> Result<Vec<String>> {
let store = webview.resources_table().get::<Store<R>>(rid)?;
async fn keys<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> Result<Vec<String>> {
let store = app.resources_table().get::<Store<R>>(rid)?;
Ok(store.keys())
}
#[tauri::command]
async fn values<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> Result<Vec<JsonValue>> {
let store = webview.resources_table().get::<Store<R>>(rid)?;
async fn values<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> Result<Vec<JsonValue>> {
let store = app.resources_table().get::<Store<R>>(rid)?;
Ok(store.values())
}
#[tauri::command]
async fn entries<R: Runtime>(
webview: Webview<R>,
app: AppHandle<R>,
rid: ResourceId,
) -> Result<Vec<(String, JsonValue)>> {
let store = webview.resources_table().get::<Store<R>>(rid)?;
let store = app.resources_table().get::<Store<R>>(rid)?;
Ok(store.entries())
}
#[tauri::command]
async fn length<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> Result<usize> {
let store = webview.resources_table().get::<Store<R>>(rid)?;
async fn length<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> Result<usize> {
let store = app.resources_table().get::<Store<R>>(rid)?;
Ok(store.length())
}
#[tauri::command]
async fn load<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> Result<()> {
let store = webview.resources_table().get::<Store<R>>(rid)?;
store.load()
async fn reload<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> Result<()> {
let store = app.resources_table().get::<Store<R>>(rid)?;
store.reload()
}
#[tauri::command]
async fn save<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> Result<()> {
let store = webview.resources_table().get::<Store<R>>(rid)?;
async fn save<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> Result<()> {
let store = app.resources_table().get::<Store<R>>(rid)?;
store.save()
}
pub trait StoreExt<R: Runtime> {
fn store(&self, path: impl AsRef<Path>) -> Store<R>;
/// Create a store or load an existing store with default settings at the given path.
///
/// If the store is already loaded, its instance is automatically returned.
///
/// # Examples
///
/// ```
/// use tauri_plugin_store::StoreExt;
///
/// tauri::Builder::default()
/// .plugin(tauri_plugin_store::Builder::default().build())
/// .setup(|app| {
/// let store = app.store("my-store")?;
/// Ok(())
/// });
/// ```
fn store(&self, path: impl AsRef<Path>) -> Result<Arc<Store<R>>>;
/// Get a store builder.
///
/// The builder can be used to configure the store.
/// To use the default settings see [`Self::store`].
///
/// # Examples
///
/// ```
/// use tauri_plugin_store::StoreExt;
/// use std::time::Duration;
///
/// tauri::Builder::default()
/// .plugin(tauri_plugin_store::Builder::default().build())
/// .setup(|app| {
/// let store = app.store_builder("users.json").auto_save(Duration::from_secs(1)).build()?;
/// Ok(())
/// });
/// ```
fn store_builder(&self, path: impl AsRef<Path>) -> StoreBuilder<R>;
/// Get a handle of an already loaded store.
///
/// If the store is not loaded or does not exist, it returns `None`.
///
/// Note that using this function can cause race conditions if you fallback to creating or loading the store,
/// so you should consider using [`Self::store`] if you are not sure if the store is loaded or not.
///
/// # Examples
///
/// ```
/// use tauri_plugin_store::StoreExt;
///
/// tauri::Builder::default()
/// .plugin(tauri_plugin_store::Builder::default().build())
/// .setup(|app| {
/// let store = if let Some(s) = app.get_store("store.json") {
/// s
/// } else {
/// // this is not thread safe; if another thread is doing the same load/create,
/// // there will be a race condition; in this case we could remove the get_store
/// // and only run app.store() as it will return the existing store if it has been loaded
/// app.store("store.json")?
/// };
/// Ok(())
/// });
/// ```
fn get_store(&self, path: impl AsRef<Path>) -> Option<Arc<Store<R>>>;
}
impl<R: Runtime, T: Manager<R>> StoreExt<R> for T {
fn store(&self, path: impl AsRef<Path>) -> Store<R> {
fn store(&self, path: impl AsRef<Path>) -> Result<Arc<Store<R>>> {
StoreBuilder::new(self.app_handle(), path).build()
}
fn store_builder(&self, path: impl AsRef<Path>) -> StoreBuilder<R> {
StoreBuilder::new(self.app_handle(), path)
}
fn get_store(&self, path: impl AsRef<Path>) -> Option<Arc<Store<R>>> {
let collection = self.state::<StoreState>();
let stores = collection.stores.lock().unwrap();
stores
.get(&resolve_store_path(self.app_handle(), path.as_ref()).ok()?)
.and_then(|rid| self.resources_table().get(*rid).ok())
}
}
// #[derive(Default)]
pub struct Builder<R: Runtime> {
stores: HashMap<PathBuf, Store<R>>,
// frozen: bool,
fn default_serialize(
cache: &HashMap<String, JsonValue>,
) -> std::result::Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
Ok(serde_json::to_vec_pretty(&cache)?)
}
impl<R: Runtime> Default for Builder<R> {
fn default_deserialize(
bytes: &[u8],
) -> std::result::Result<HashMap<String, JsonValue>, Box<dyn std::error::Error + Send + Sync>> {
serde_json::from_slice(bytes).map_err(Into::into)
}
pub struct Builder {
serialize_fns: HashMap<String, SerializeFn>,
deserialize_fns: HashMap<String, DeserializeFn>,
default_serialize: SerializeFn,
default_deserialize: DeserializeFn,
}
impl Default for Builder {
fn default() -> Self {
Self {
stores: Default::default(),
// frozen: false,
serialize_fns: Default::default(),
deserialize_fns: Default::default(),
default_serialize,
default_deserialize,
}
}
}
impl<R: Runtime> Builder<R> {
impl Builder {
pub fn new() -> Self {
Self::default()
}
// /// Registers a store with the plugin.
// ///
// /// # Examples
// ///
// /// ```
// /// use tauri_plugin_store::{StoreBuilder, Builder};
// ///
// /// tauri::Builder::default()
// /// .setup(|app| {
// /// let store = StoreBuilder::new("store.bin").build(app.handle().clone());
// /// let builder = Builder::default().store(store);
// /// Ok(())
// /// });
// /// ```
// pub fn store(mut self, store: Store<R>) -> Self {
// self.stores.insert(store.path.clone(), store);
// self
// }
/// Register a serialize function to access it from the JavaScript side
///
/// # Examples
///
/// ```
/// fn no_pretty_json(
/// cache: &std::collections::HashMap<String, serde_json::Value>,
/// ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
/// Ok(serde_json::to_vec(&cache)?)
/// }
///
/// tauri::Builder::default()
/// .plugin(
/// tauri_plugin_store::Builder::default()
/// .register_serialize_fn("no-pretty-json".to_owned(), no_pretty_json)
/// .build(),
/// );
/// ```
pub fn register_serialize_fn(mut self, name: String, serialize_fn: SerializeFn) -> Self {
self.serialize_fns.insert(name, serialize_fn);
self
}
// /// Registers multiple stores with the plugin.
// ///
// /// # Examples
// ///
// /// ```
// /// use tauri_plugin_store::{StoreBuilder, Builder};
// ///
// /// tauri::Builder::default()
// /// .setup(|app| {
// /// let store = StoreBuilder::new("store.bin").build(app.handle().clone());
// /// let builder = Builder::default().stores([store]);
// /// Ok(())
// /// });
// /// ```
// pub fn stores<T: IntoIterator<Item = Store<R>>>(mut self, stores: T) -> Self {
// self.stores = stores
// .into_iter()
// .map(|store| (store.path.clone(), store))
// .collect();
// self
// }
/// Register a deserialize function to access it from the JavaScript side
pub fn register_deserialize_fn(mut self, name: String, deserialize_fn: DeserializeFn) -> Self {
self.deserialize_fns.insert(name, deserialize_fn);
self
}
// /// Freezes the collection.
// ///
// /// This causes requests for plugins that haven't been registered to fail
// ///
// /// # Examples
// ///
// /// ```
// /// use tauri_plugin_store::{StoreBuilder, Builder};
// ///
// /// tauri::Builder::default()
// /// .setup(|app| {
// /// let store = StoreBuilder::new("store.bin").build(app.handle().clone());
// /// app.handle().plugin(Builder::default().freeze().build());
// /// Ok(())
// /// });
// /// ```
// pub fn freeze(mut self) -> Self {
// self.frozen = true;
// self
// }
/// Use this serialize function for stores by default
///
/// # Examples
///
/// ```
/// fn no_pretty_json(
/// cache: &std::collections::HashMap<String, serde_json::Value>,
/// ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
/// Ok(serde_json::to_vec(&cache)?)
/// }
///
/// tauri::Builder::default()
/// .plugin(
/// tauri_plugin_store::Builder::default()
/// .default_serialize_fn(no_pretty_json)
/// .build(),
/// );
/// ```
pub fn default_serialize_fn(mut self, serialize_fn: SerializeFn) -> Self {
self.default_serialize = serialize_fn;
self
}
/// Use this deserialize function for stores by default
pub fn default_deserialize_fn(mut self, deserialize_fn: DeserializeFn) -> Self {
self.default_deserialize = deserialize_fn;
self
}
/// Builds the plugin.
///
@@ -249,56 +407,37 @@ impl<R: Runtime> Builder<R> {
/// tauri::Builder::default()
/// .plugin(tauri_plugin_store::Builder::default().build())
/// .setup(|app| {
/// let store = tauri_plugin_store::StoreBuilder::new(app, "store.bin").build();
/// let store = tauri_plugin_store::StoreBuilder::new(app, "store.bin").build()?;
/// Ok(())
/// });
/// ```
pub fn build(mut self) -> TauriPlugin<R> {
pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
plugin::Builder::new("store")
.invoke_handler(tauri::generate_handler![
create_store,
set,
get,
has,
delete,
clear,
reset,
keys,
values,
length,
entries,
load,
save
load, get_store, set, get, has, delete, clear, reset, keys, values, length,
entries, reload, save,
])
.setup(move |app_handle, _api| {
for (path, store) in self.stores.iter_mut() {
// ignore loading errors, just use the default
if let Err(err) = store.load() {
warn!(
"Failed to load store {path:?} from disk: {err}. Falling back to default values."
);
}
}
app_handle.manage(StoreCollection::<R> {
stores: Mutex::new(HashMap::new()),
// frozen: self.frozen,
app_handle.manage(StoreState {
stores: Arc::new(Mutex::new(HashMap::new())),
serialize_fns: self.serialize_fns,
deserialize_fns: self.deserialize_fns,
default_serialize: self.default_serialize,
default_deserialize: self.default_deserialize,
});
Ok(())
})
.on_event(|_app_handle, event| {
.on_event(|app_handle, event| {
if let RunEvent::Exit = event {
// let collection = app_handle.state::<StoreCollection<R>>();
// for store in collection.stores.lock().expect("mutex poisoned").values_mut() {
// if let Some(sender) = store.auto_save_debounce_sender.take() {
// let _ = sender.send(AutoSaveMessage::Cancel);
// }
// if let Err(err) = store.save() {
// eprintln!("failed to save store {:?} with error {:?}", store.path, err);
// }
// }
let collection = app_handle.state::<StoreState>();
let stores = collection.stores.lock().unwrap();
for (path, rid) in stores.iter() {
if let Ok(store) = app_handle.resources_table().get::<Store<R>>(*rid) {
if let Err(err) = store.save() {
log::error!("failed to save store {path:?} with error {err:?}");
}
}
}
}
})
.build()
+216 -122
View File
@@ -2,38 +2,32 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{ChangePayload, StoreCollection};
use crate::{ChangePayload, StoreState};
use serde_json::Value as JsonValue;
use std::{
collections::HashMap,
fs::{create_dir_all, read, File},
io::Write,
fs,
path::{Path, PathBuf},
sync::{Arc, Mutex},
time::Duration,
};
use tauri::{AppHandle, Emitter, Manager, Resource, Runtime};
use tauri::{path::BaseDirectory, AppHandle, Emitter, Manager, Resource, ResourceId, Runtime};
use tokio::{
select,
sync::mpsc::{unbounded_channel, UnboundedSender},
time::sleep,
};
type SerializeFn =
pub type SerializeFn =
fn(&HashMap<String, JsonValue>) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>>;
pub(crate) type DeserializeFn =
pub type DeserializeFn =
fn(&[u8]) -> Result<HashMap<String, JsonValue>, Box<dyn std::error::Error + Send + Sync>>;
fn default_serialize(
cache: &HashMap<String, JsonValue>,
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
Ok(serde_json::to_vec(&cache)?)
}
fn default_deserialize(
bytes: &[u8],
) -> Result<HashMap<String, JsonValue>, Box<dyn std::error::Error + Send + Sync>> {
serde_json::from_slice(bytes).map_err(Into::into)
pub fn resolve_store_path<R: Runtime>(
app: &AppHandle<R>,
path: impl AsRef<Path>,
) -> crate::Result<PathBuf> {
Ok(dunce::simplified(&app.path().resolve(path, BaseDirectory::AppData)?).to_path_buf())
}
/// Builds a [`Store`]
@@ -41,10 +35,10 @@ pub struct StoreBuilder<R: Runtime> {
app: AppHandle<R>,
path: PathBuf,
defaults: Option<HashMap<String, JsonValue>>,
cache: HashMap<String, JsonValue>,
serialize: SerializeFn,
deserialize: DeserializeFn,
serialize_fn: SerializeFn,
deserialize_fn: DeserializeFn,
auto_save: Option<Duration>,
create_new: bool,
}
impl<R: Runtime> StoreBuilder<R> {
@@ -60,15 +54,18 @@ impl<R: Runtime> StoreBuilder<R> {
/// });
/// ```
pub fn new<M: Manager<R>, P: AsRef<Path>>(manager: &M, path: P) -> Self {
let app = manager.app_handle().clone();
let state = app.state::<StoreState>();
let serialize_fn = state.default_serialize;
let deserialize_fn = state.default_deserialize;
Self {
app: manager.app_handle().clone(),
// Since Store.path is only exposed to the user in emit calls we may as well simplify it here already.
path: dunce::simplified(path.as_ref()).to_path_buf(),
app,
path: path.as_ref().to_path_buf(),
defaults: None,
cache: Default::default(),
serialize: default_serialize,
deserialize: default_deserialize,
auto_save: None,
serialize_fn,
deserialize_fn,
auto_save: Some(Duration::from_millis(100)),
create_new: false,
}
}
@@ -84,17 +81,16 @@ impl<R: Runtime> StoreBuilder<R> {
///
/// let store = tauri_plugin_store::StoreBuilder::new(app, "store.bin")
/// .defaults(defaults)
/// .build();
/// .build()?;
/// Ok(())
/// });
/// ```
pub fn defaults(mut self, defaults: HashMap<String, JsonValue>) -> Self {
self.cache.clone_from(&defaults);
self.defaults = Some(defaults);
self
}
/// Inserts multiple key-value pairs.
/// Inserts multiple default key-value pairs.
///
/// # Examples
/// ```
@@ -103,14 +99,13 @@ impl<R: Runtime> StoreBuilder<R> {
/// .setup(|app| {
/// let store = tauri_plugin_store::StoreBuilder::new(app, "store.bin")
/// .default("foo".to_string(), "bar")
/// .build();
/// .build()?;
/// Ok(())
/// });
/// ```
pub fn default(mut self, key: impl Into<String>, value: impl Into<JsonValue>) -> Self {
let key = key.into();
let value = value.into();
self.cache.insert(key.clone(), value.clone());
self.defaults
.get_or_insert(HashMap::new())
.insert(key, value);
@@ -126,12 +121,12 @@ impl<R: Runtime> StoreBuilder<R> {
/// .setup(|app| {
/// let store = tauri_plugin_store::StoreBuilder::new(app, "store.json")
/// .serialize(|cache| serde_json::to_vec(&cache).map_err(Into::into))
/// .build();
/// .build()?;
/// Ok(())
/// });
/// ```
pub fn serialize(mut self, serialize: SerializeFn) -> Self {
self.serialize = serialize;
self.serialize_fn = serialize;
self
}
@@ -144,28 +139,25 @@ impl<R: Runtime> StoreBuilder<R> {
/// .setup(|app| {
/// let store = tauri_plugin_store::StoreBuilder::new(app, "store.json")
/// .deserialize(|bytes| serde_json::from_slice(&bytes).map_err(Into::into))
/// .build();
/// .build()?;
/// Ok(())
/// });
/// ```
pub fn deserialize(mut self, deserialize: DeserializeFn) -> Self {
self.deserialize = deserialize;
self.deserialize_fn = deserialize;
self
}
/// Auto save on modified with a debounce duration
///
/// Note: only works if this store is managed by the plugin (e.g. made using [`crate::with_store`] or inserted into [`crate::Builder`])
///
/// # Examples
/// ```
///
/// tauri::Builder::default()
/// .plugin(tauri_plugin_store::Builder::default().build())
/// .setup(|app| {
/// let store = tauri_plugin_store::StoreBuilder::new(app, "store.json")
/// .auto_save(std::time::Duration::from_millis(100))
/// .build();
/// .build()?;
/// Ok(())
/// });
/// ```
@@ -174,7 +166,64 @@ impl<R: Runtime> StoreBuilder<R> {
self
}
/// Builds the [`Store`].
/// Disable auto save on modified with a debounce duration.
pub fn disable_auto_save(mut self) -> Self {
self.auto_save = None;
self
}
/// Force create a new store with default values even if it already exists.
pub fn create_new(mut self) -> Self {
self.create_new = true;
self
}
pub(crate) fn build_inner(mut self) -> crate::Result<(Arc<Store<R>>, ResourceId)> {
let stores = self.app.state::<StoreState>().stores.clone();
let mut stores = stores.lock().unwrap();
self.path = resolve_store_path(&self.app, self.path)?;
if self.create_new {
if let Some(rid) = stores.remove(&self.path) {
let _ = self.app.resources_table().take::<Store<R>>(rid);
}
} else if let Some(rid) = stores.get(&self.path) {
return Ok((self.app.resources_table().get(*rid).unwrap(), *rid));
}
// if stores.contains_key(&self.path) {
// return Err(crate::Error::AlreadyExists(self.path));
// }
let mut store_inner = StoreInner::new(
self.app.clone(),
self.path.clone(),
self.defaults.take(),
self.serialize_fn,
self.deserialize_fn,
);
if !self.create_new {
let _ = store_inner.load();
}
let store = Store {
auto_save: self.auto_save,
auto_save_debounce_sender: Arc::new(Mutex::new(None)),
store: Arc::new(Mutex::new(store_inner)),
};
let store = Arc::new(store);
let rid = self.app.resources_table().add_arc(store.clone());
stores.insert(self.path, rid);
Ok((store, rid))
}
/// Load the existing store with the same path or creates a new [`Store`].
///
/// If a store with the same path has already been loaded its instance is returned.
///
/// # Examples
/// ```
@@ -185,131 +234,116 @@ impl<R: Runtime> StoreBuilder<R> {
/// Ok(())
/// });
/// ```
pub fn build(self) -> Store<R> {
let collection = self.app.state::<StoreCollection<R>>();
let mut stores = collection.stores.lock().unwrap();
let store = stores
.get(&self.path)
.and_then(|store| store.upgrade())
.unwrap_or_else(|| {
let mut store = StoreInner::new(self.app.clone(), self.path.clone());
let _ = store.load(self.deserialize);
let store = Arc::new(Mutex::new(store));
stores.insert(
self.path.clone(),
Arc::<Mutex<StoreInner<R>>>::downgrade(&store),
);
store
});
drop(stores);
Store {
defaults: self.defaults,
serialize: self.serialize,
deserialize: self.deserialize,
auto_save: self.auto_save,
auto_save_debounce_sender: Arc::new(Mutex::new(None)),
store,
}
pub fn build(self) -> crate::Result<Arc<Store<R>>> {
let (store, _) = self.build_inner()?;
Ok(store)
}
}
pub(crate) enum AutoSaveMessage {
enum AutoSaveMessage {
Reset,
Cancel,
}
#[derive(Clone)]
pub struct StoreInner<R: Runtime> {
pub(crate) app: AppHandle<R>,
pub(crate) path: PathBuf,
pub(crate) cache: HashMap<String, JsonValue>,
struct StoreInner<R: Runtime> {
app: AppHandle<R>,
path: PathBuf,
cache: HashMap<String, JsonValue>,
defaults: Option<HashMap<String, JsonValue>>,
serialize_fn: SerializeFn,
deserialize_fn: DeserializeFn,
}
impl<R: Runtime> StoreInner<R> {
pub fn new(app: AppHandle<R>, path: PathBuf) -> Self {
fn new(
app: AppHandle<R>,
path: PathBuf,
defaults: Option<HashMap<String, JsonValue>>,
serialize_fn: SerializeFn,
deserialize_fn: DeserializeFn,
) -> Self {
Self {
app,
path,
cache: HashMap::new(),
cache: defaults.clone().unwrap_or_default(),
defaults,
serialize_fn,
deserialize_fn,
}
}
pub fn save(&self, serialize_fn: SerializeFn) -> crate::Result<()> {
let app_dir = self
.app
.path()
.app_data_dir()
.expect("failed to resolve app dir");
let store_path = app_dir.join(&self.path);
/// Saves the store to disk at the store's `path`.
pub fn save(&self) -> crate::Result<()> {
fs::create_dir_all(self.path.parent().expect("invalid store path"))?;
create_dir_all(store_path.parent().expect("invalid store path"))?;
let bytes = serialize_fn(&self.cache).map_err(crate::Error::Serialize)?;
let mut f = File::create(&store_path)?;
f.write_all(&bytes)?;
let bytes = (self.serialize_fn)(&self.cache).map_err(crate::Error::Serialize)?;
fs::write(&self.path, bytes)?;
Ok(())
}
/// Update the store from the on-disk state
pub fn load(&mut self, deserialize_fn: DeserializeFn) -> crate::Result<()> {
let app_dir = self
.app
.path()
.app_data_dir()
.expect("failed to resolve app dir");
let store_path = app_dir.join(&self.path);
let bytes = read(store_path)?;
pub fn load(&mut self) -> crate::Result<()> {
let bytes = fs::read(&self.path)?;
self.cache
.extend(deserialize_fn(&bytes).map_err(crate::Error::Deserialize)?);
.extend((self.deserialize_fn)(&bytes).map_err(crate::Error::Deserialize)?);
Ok(())
}
pub fn insert(&mut self, key: impl Into<String>, value: impl Into<JsonValue>) {
/// Inserts a key-value pair into the store.
pub fn set(&mut self, key: impl Into<String>, value: impl Into<JsonValue>) {
let key = key.into();
let value = value.into();
self.cache.insert(key.clone(), value.clone());
let _ = self.emit_change_event(&key, &value);
let _ = self.emit_change_event(&key, Some(&value));
}
/// Returns a reference to the value corresponding to the key.
pub fn get(&self, key: impl AsRef<str>) -> Option<&JsonValue> {
self.cache.get(key.as_ref())
}
/// Returns `true` if the given `key` exists in the store.
pub fn has(&self, key: impl AsRef<str>) -> bool {
self.cache.contains_key(key.as_ref())
}
/// Removes a key-value pair from the store.
pub fn delete(&mut self, key: impl AsRef<str>) -> bool {
let flag = self.cache.remove(key.as_ref()).is_some();
if flag {
let _ = self.emit_change_event(key.as_ref(), &JsonValue::Null);
let _ = self.emit_change_event(key.as_ref(), None);
}
flag
}
/// Clears the store, removing all key-value pairs.
///
/// Note: To clear the storage and reset it to its `default` value, use [`reset`](Self::reset) instead.
pub fn clear(&mut self) {
let keys: Vec<String> = self.cache.keys().cloned().collect();
self.cache.clear();
for key in &keys {
let _ = self.emit_change_event(key, &JsonValue::Null);
let _ = self.emit_change_event(key, None);
}
}
pub fn reset(&mut self, defaults: &Option<HashMap<String, JsonValue>>) {
if let Some(defaults) = &defaults {
/// Resets the store to its `default` value.
///
/// If no default value has been set, this method behaves identical to [`clear`](Self::clear).
pub fn reset(&mut self) {
if let Some(defaults) = &self.defaults {
for (key, value) in &self.cache {
if defaults.get(key) != Some(value) {
let _ =
self.emit_change_event(key, defaults.get(key).unwrap_or(&JsonValue::Null));
let _ = self.emit_change_event(key, defaults.get(key));
}
}
for (key, value) in defaults {
if !self.cache.contains_key(key) {
let _ = self.emit_change_event(key, value);
let _ = self.emit_change_event(key, Some(value));
}
}
self.cache.clone_from(defaults);
@@ -318,33 +352,43 @@ impl<R: Runtime> StoreInner<R> {
}
}
/// An iterator visiting all keys in arbitrary order.
pub fn keys(&self) -> impl Iterator<Item = &String> {
self.cache.keys()
}
/// An iterator visiting all values in arbitrary order.
pub fn values(&self) -> impl Iterator<Item = &JsonValue> {
self.cache.values()
}
/// An iterator visiting all key-value pairs in arbitrary order.
pub fn entries(&self) -> impl Iterator<Item = (&String, &JsonValue)> {
self.cache.iter()
}
/// Returns the number of elements in the store.
pub fn len(&self) -> usize {
self.cache.len()
}
/// Returns true if the store contains no elements.
pub fn is_empty(&self) -> bool {
self.cache.is_empty()
}
fn emit_change_event(&self, key: &str, value: &JsonValue) -> crate::Result<()> {
fn emit_change_event(&self, key: &str, value: Option<&JsonValue>) -> crate::Result<()> {
let state = self.app.state::<StoreState>();
let stores = state.stores.lock().unwrap();
let exists = value.is_some();
self.app.emit(
"store://change",
ChangePayload {
path: &self.path,
resource_id: stores.get(&self.path).copied(),
key,
value,
exists,
},
)?;
Ok(())
@@ -361,38 +405,45 @@ impl<R: Runtime> std::fmt::Debug for StoreInner<R> {
}
pub struct Store<R: Runtime> {
defaults: Option<HashMap<String, JsonValue>>,
serialize: SerializeFn,
deserialize: DeserializeFn,
auto_save: Option<Duration>,
auto_save_debounce_sender: Arc<Mutex<Option<UnboundedSender<AutoSaveMessage>>>>,
store: Arc<Mutex<StoreInner<R>>>,
}
impl<R: Runtime> Resource for Store<R> {}
impl<R: Runtime> Resource for Store<R> {
fn close(self: Arc<Self>) {
let store = self.store.lock().unwrap();
let state = store.app.state::<StoreState>();
let mut stores = state.stores.lock().unwrap();
stores.remove(&store.path);
}
}
impl<R: Runtime> Store<R> {
pub fn with_store<T>(
&self,
f: impl FnOnce(&mut StoreInner<R>) -> crate::Result<T>,
) -> crate::Result<T> {
let mut store = self.store.lock().unwrap();
f(&mut store)
}
// /// Do something with the inner store,
// /// useful for batching some work if you need higher performance
// pub fn with_store<T>(&self, f: impl FnOnce(&mut StoreInner<R>) -> T) -> T {
// let mut store = self.store.lock().unwrap();
// f(&mut store)
// }
/// Inserts a key-value pair into the store.
pub fn set(&self, key: impl Into<String>, value: impl Into<JsonValue>) {
self.store.lock().unwrap().insert(key.into(), value.into());
self.store.lock().unwrap().set(key.into(), value.into());
let _ = self.trigger_auto_save();
}
/// Returns the value for the given `key` or `None` if the key does not exist.
pub fn get(&self, key: impl AsRef<str>) -> Option<JsonValue> {
self.store.lock().unwrap().get(key).cloned()
}
/// Returns `true` if the given `key` exists in the store.
pub fn has(&self, key: impl AsRef<str>) -> bool {
self.store.lock().unwrap().has(key)
}
/// Removes a key-value pair from the store.
pub fn delete(&self, key: impl AsRef<str>) -> bool {
let deleted = self.store.lock().unwrap().delete(key);
if deleted {
@@ -401,24 +452,33 @@ impl<R: Runtime> Store<R> {
deleted
}
/// Clears the store, removing all key-value pairs.
///
/// Note: To clear the storage and reset it to its `default` value, use [`reset`](Self::reset) instead.
pub fn clear(&self) {
self.store.lock().unwrap().clear();
let _ = self.trigger_auto_save();
}
/// Resets the store to its `default` value.
///
/// If no default value has been set, this method behaves identical to [`clear`](Self::clear).
pub fn reset(&self) {
self.store.lock().unwrap().reset(&self.defaults);
self.store.lock().unwrap().reset();
let _ = self.trigger_auto_save();
}
/// Returns a list of all keys in the store.
pub fn keys(&self) -> Vec<String> {
self.store.lock().unwrap().keys().cloned().collect()
}
/// Returns a list of all values in the store.
pub fn values(&self) -> Vec<JsonValue> {
self.store.lock().unwrap().values().cloned().collect()
}
/// Returns a list of all key-value pairs in the store.
pub fn entries(&self) -> Vec<(String, JsonValue)> {
self.store
.lock()
@@ -428,19 +488,40 @@ impl<R: Runtime> Store<R> {
.collect()
}
/// Returns the number of elements in the store.
pub fn length(&self) -> usize {
self.store.lock().unwrap().len()
}
pub fn load(&self) -> crate::Result<()> {
self.store.lock().unwrap().load(self.deserialize)
/// Returns true if the store contains no elements.
pub fn is_empty(&self) -> bool {
self.store.lock().unwrap().is_empty()
}
/// Update the store from the on-disk state
pub fn reload(&self) -> crate::Result<()> {
self.store.lock().unwrap().load()
}
/// Saves the store to disk at the store's `path`.
pub fn save(&self) -> crate::Result<()> {
if let Some(sender) = self.auto_save_debounce_sender.lock().unwrap().take() {
let _ = sender.send(AutoSaveMessage::Cancel);
}
self.store.lock().unwrap().save(self.serialize)
self.store.lock().unwrap().save()
}
/// Removes the store from the resource table
pub fn close_resource(&self) {
let store = self.store.lock().unwrap();
let app = store.app.clone();
let state = app.state::<StoreState>();
let stores = state.stores.lock().unwrap();
if let Some(rid) = stores.get(&store.path).copied() {
drop(store);
drop(stores);
let _ = app.resources_table().close(rid);
}
}
fn trigger_auto_save(&self) -> crate::Result<()> {
@@ -459,7 +540,6 @@ impl<R: Runtime> Store<R> {
auto_save_debounce_sender.replace(sender);
drop(auto_save_debounce_sender);
let store = self.store.clone();
let serialize_fn = self.serialize;
let auto_save_debounce_sender = self.auto_save_debounce_sender.clone();
tauri::async_runtime::spawn(async move {
loop {
@@ -471,7 +551,7 @@ impl<R: Runtime> Store<R> {
}
_ = sleep(auto_save_delay) => {
auto_save_debounce_sender.lock().unwrap().take();
let _ = store.lock().unwrap().save(serialize_fn);
let _ = store.lock().unwrap().save();
return;
}
};
@@ -479,4 +559,18 @@ impl<R: Runtime> Store<R> {
});
Ok(())
}
fn apply_pending_auto_save(&self) {
// Cancel and save if auto save is pending
if let Some(sender) = self.auto_save_debounce_sender.lock().unwrap().take() {
let _ = sender.send(AutoSaveMessage::Cancel);
let _ = self.save();
};
}
}
impl<R: Runtime> Drop for Store<R> {
fn drop(&mut self) {
self.apply_pending_auto_save();
}
}
+4
View File
@@ -1,5 +1,9 @@
# Changelog
## \[2.1.0]
- [`87cc5852`](https://github.com/tauri-apps/plugins-workspace/commit/87cc58527d769960427a2f46bb10532f5dcf7ace) ([#1797](https://github.com/tauri-apps/plugins-workspace/pull/1797) by [@VirtualPirate](https://github.com/tauri-apps/plugins-workspace/../../VirtualPirate)) Added feature for calculating `transfer_speed` during file uploads and downloads
## \[2.0.1]
- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-upload"
version = "2.0.1"
version = "2.1.0"
description = "Upload files from disk to a remote server over HTTP."
authors = { workspace = true }
license = { workspace = true }
+1
View File
@@ -7,6 +7,7 @@ import { invoke, Channel } from '@tauri-apps/api/core'
interface ProgressPayload {
progress: number
total: number
transferSpeed: number
}
type ProgressHandler = (progress: ProgressPayload) => void
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-upload",
"version": "2.0.0",
"version": "2.1.0",
"description": "Upload files from disk to a remote server over HTTP.",
"license": "MIT OR Apache-2.0",
"authors": [
+15 -1
View File
@@ -13,6 +13,9 @@
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
mod transfer_stats;
use transfer_stats::TransferStats;
use futures_util::TryStreamExt;
use serde::{ser::Serializer, Serialize};
use tauri::{
@@ -55,9 +58,11 @@ impl Serialize for Error {
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct ProgressPayload {
progress: u64,
total: u64,
transfer_speed: u64,
}
#[command]
@@ -88,11 +93,14 @@ async fn download(
let mut file = BufWriter::new(File::create(file_path).await?);
let mut stream = response.bytes_stream();
let mut stats = TransferStats::default();
while let Some(chunk) = stream.try_next().await? {
file.write_all(&chunk).await?;
stats.record_chunk_transfer(chunk.len());
let _ = on_progress.send(ProgressPayload {
progress: chunk.len() as u64,
total,
transfer_speed: stats.transfer_speed,
});
}
file.flush().await?;
@@ -138,10 +146,16 @@ async fn upload(
fn file_to_body(channel: Channel<ProgressPayload>, file: File) -> reqwest::Body {
let stream = FramedRead::new(file, BytesCodec::new()).map_ok(|r| r.freeze());
let mut stats = TransferStats::default();
reqwest::Body::wrap_stream(ReadProgressStream::new(
stream,
Box::new(move |progress, total| {
let _ = channel.send(ProgressPayload { progress, total });
stats.record_chunk_transfer(progress as usize);
let _ = channel.send(ProgressPayload {
progress,
total,
transfer_speed: stats.transfer_speed,
});
}),
))
}

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