mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-06-18 14:40:07 +02:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fd529ae22 | |||
| f71be86d47 | |||
| 7d9dfaeb9e | |||
| 2c00f7ddad | |||
| 0f310e75ed | |||
| bf3e8a0b8a | |||
| 3fa0fc09bb | |||
| fef76bd504 | |||
| f8f2eefe03 | |||
| c665818395 | |||
| 2ba68760b9 | |||
| 5070476816 | |||
| 51cd283a5f | |||
| d44f0ee7a7 | |||
| b1b0565d12 | |||
| db526a1c97 | |||
| a3b553ddb4 | |||
| fecfd5533a | |||
| ed981027dd | |||
| 5092ea5e89 | |||
| ac2edc2159 | |||
| 59dd5f105a | |||
| 4db626354c | |||
| 383e636a8e | |||
| 1051db406a | |||
| ee3fb1dba6 | |||
| c34b2ea824 | |||
| 8a33595bbe | |||
| ff05a59e60 | |||
| bea474c550 | |||
| e5476aac94 | |||
| 7f025e5240 | |||
| 5700bd2213 | |||
| d402c3865a | |||
| 90ef77c872 | |||
| 51856e9e0a | |||
| 9741b97e8c | |||
| e421b9a2c0 | |||
| 371a2f7361 | |||
| 52c093ac9d | |||
| 6d6508f18e | |||
| 3fa814d1f0 | |||
| 1fe3dab64c | |||
| 5dadd205f5 | |||
| 3e15acea9a | |||
| 3e78173df9 | |||
| 64fac08bfb | |||
| fdc382dff0 | |||
| b2aea04567 | |||
| 3449dd5a8f |
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
fs: minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add the `size` method to get the size of a file or directory.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"upload": "minor"
|
||||||
|
"upload-js": "minor"
|
||||||
|
---
|
||||||
|
|
||||||
|
Added a new field `progressTotal` to track the total amount of data transferred during the upload/download process.
|
||||||
@@ -62,6 +62,7 @@
|
|||||||
"dialog",
|
"dialog",
|
||||||
"fs",
|
"fs",
|
||||||
"global-shortcut",
|
"global-shortcut",
|
||||||
|
"opener",
|
||||||
"http",
|
"http",
|
||||||
"nfc",
|
"nfc",
|
||||||
"notification",
|
"notification",
|
||||||
@@ -87,6 +88,7 @@
|
|||||||
"dialog-js",
|
"dialog-js",
|
||||||
"fs-js",
|
"fs-js",
|
||||||
"global-shortcut-js",
|
"global-shortcut-js",
|
||||||
|
"opener-js",
|
||||||
"http-js",
|
"http-js",
|
||||||
"nfc-js",
|
"nfc-js",
|
||||||
"notification-js",
|
"notification-js",
|
||||||
@@ -186,6 +188,14 @@
|
|||||||
"path": "./plugins/global-shortcut",
|
"path": "./plugins/global-shortcut",
|
||||||
"manager": "javascript"
|
"manager": "javascript"
|
||||||
},
|
},
|
||||||
|
"opener": {
|
||||||
|
"path": "./plugins/opener",
|
||||||
|
"manager": "rust"
|
||||||
|
},
|
||||||
|
"opener-js": {
|
||||||
|
"path": "./plugins/opener",
|
||||||
|
"manager": "javascript"
|
||||||
|
},
|
||||||
"haptics": {
|
"haptics": {
|
||||||
"path": "./plugins/haptics",
|
"path": "./plugins/haptics",
|
||||||
"manager": "rust"
|
"manager": "rust"
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"updater": "minor"
|
||||||
|
---
|
||||||
|
|
||||||
|
Added support for `.deb` package updates on Linux systems.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
clipboard-manager-js: patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix clipboard manager client side api not copying fallback alternative text when calling `writeHtml`.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
deep-link: patch
|
||||||
|
deep-link-js: patch
|
||||||
|
---
|
||||||
|
|
||||||
|
`onOpenUrl()` will now not call `getCurrent()` anymore, matching the documented behavior.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
fs: minor
|
||||||
|
persisted-scope: minor
|
||||||
|
---
|
||||||
|
|
||||||
|
**Breaking Change:** Replaced the custom `tauri_plugin_fs::Scope` struct with `tauri::fs::Scope`.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
'log-plugin': 'patch'
|
||||||
|
'log-js': 'patch'
|
||||||
|
---
|
||||||
|
|
||||||
|
Make webview log target more consistent that it always starts with `webview`
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"fs": "patch"
|
||||||
|
"fs-js": "patch"
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve performance of `readTextFile` and `readTextFileLines` APIs
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"fs": "patch"
|
||||||
|
"fs-js": "patch"
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix `readDir` function failing to read directories that contain broken symlinks.
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"fs": "patch"
|
||||||
|
"fs-js": "patch"
|
||||||
|
---
|
||||||
|
|
||||||
|
Add support for using `ReadableStream<Unit8Array>` with `writeFile` API.
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"http": "patch"
|
||||||
|
---
|
||||||
|
|
||||||
|
Add tracing logs for requestes and responses behind `tracing` feature flag.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'localhost': 'minor'
|
||||||
|
---
|
||||||
|
|
||||||
|
Add custom host binding to allow external access
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"opener": "major"
|
||||||
|
"opener-js": "major"
|
||||||
|
---
|
||||||
|
|
||||||
|
Initial Release
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"positioner-js": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add `moveWindowConstrained` function that is similar to `moveWindow` but constrains the window to the screen dimensions in case of tray icon positions.
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"positioner": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add `WindowExt::move_window_constrained` method that is similar to `WindowExt::move_window` but constrains the window to the screen dimensions in case of tray icon positions.
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
shell: patch
|
||||||
|
---
|
||||||
|
|
||||||
|
shell.open will now try to execute `/usr/bin/xdg-open` before using `xdg-open` from `PATH`.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"sql": "patch"
|
||||||
|
---
|
||||||
|
|
||||||
|
Allow blocking on async code without creating a nested runtime.
|
||||||
@@ -53,6 +53,10 @@ jobs:
|
|||||||
- .github/workflows/check-generated-files.yml
|
- .github/workflows/check-generated-files.yml
|
||||||
- plugins/global-shortcut/guest-js/**
|
- plugins/global-shortcut/guest-js/**
|
||||||
- plugins/global-shortcut/src/api-iife.js
|
- plugins/global-shortcut/src/api-iife.js
|
||||||
|
opener:
|
||||||
|
- .github/workflows/check-generated-files.yml
|
||||||
|
- plugins/opener/guest-js/**
|
||||||
|
- plugins/opener/src/api-iife.js
|
||||||
haptics:
|
haptics:
|
||||||
- .github/workflows/check-generated-files.yml
|
- .github/workflows/check-generated-files.yml
|
||||||
- plugins/haptics/guest-js/**
|
- plugins/haptics/guest-js/**
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ jobs:
|
|||||||
tauri-plugin-global-shortcut:
|
tauri-plugin-global-shortcut:
|
||||||
- .github/workflows/lint-rust.yml
|
- .github/workflows/lint-rust.yml
|
||||||
- plugins/global-shortcut/**
|
- plugins/global-shortcut/**
|
||||||
|
tauri-plugin-opener:
|
||||||
|
- .github/workflows/lint-rust.yml
|
||||||
|
- plugins/opener/**
|
||||||
tauri-plugin-haptics:
|
tauri-plugin-haptics:
|
||||||
- .github/workflows/lint-rust.yml
|
- .github/workflows/lint-rust.yml
|
||||||
- plugins/haptics/**
|
- plugins/haptics/**
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ jobs:
|
|||||||
- .github/workflows/test-rust.yml
|
- .github/workflows/test-rust.yml
|
||||||
- Cargo.toml
|
- Cargo.toml
|
||||||
- plugins/global-shortcut/**
|
- plugins/global-shortcut/**
|
||||||
|
tauri-plugin-opener:
|
||||||
|
- .github/workflows/test-rust.yml
|
||||||
|
- Cargo.toml
|
||||||
|
- plugins/opener/**
|
||||||
tauri-plugin-haptics:
|
tauri-plugin-haptics:
|
||||||
- .github/workflows/test-rust.yml
|
- .github/workflows/test-rust.yml
|
||||||
- Cargo.toml
|
- Cargo.toml
|
||||||
|
|||||||
Generated
+838
-439
File diff suppressed because it is too large
Load Diff
+8
-5
@@ -10,17 +10,20 @@ resolver = "2"
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
tracing = "0.1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
tauri = { version = "2.0.4", default-features = false }
|
tauri = { version = "2", default-features = false }
|
||||||
tauri-build = "2.0.1"
|
tauri-build = "2"
|
||||||
tauri-plugin = "2.0.1"
|
tauri-plugin = "2"
|
||||||
tauri-utils = "2.0.1"
|
tauri-utils = "2"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
thiserror = "1"
|
thiserror = "2"
|
||||||
url = "2"
|
url = "2"
|
||||||
schemars = "0.8"
|
schemars = "0.8"
|
||||||
dunce = "1"
|
dunce = "1"
|
||||||
specta = "=2.0.0-rc.20"
|
specta = "=2.0.0-rc.20"
|
||||||
|
glob = "0.3"
|
||||||
|
zbus = "4"
|
||||||
#tauri-specta = "=2.0.0-rc.11"
|
#tauri-specta = "=2.0.0-rc.11"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ This repo and all plugins require a Rust version of at least **1.77.2**
|
|||||||
| [log](plugins/log) | Configurable logging. | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| [log](plugins/log) | Configurable logging. | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| [nfc](plugins/nfc) | Read and write NFC tags on Android and iOS. | ? | ? | ? | ✅ | ✅ |
|
| [nfc](plugins/nfc) | Read and write NFC tags on Android and iOS. | ? | ? | ? | ✅ | ✅ |
|
||||||
| [notification](plugins/notification) | Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API. | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| [notification](plugins/notification) | Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API. | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| [opener](plugins/opener) | Open files and URLs using their default application. | ✅ | ✅ | ✅ | ? | ? |
|
||||||
| [os](plugins/os) | Read information about the operating system. | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| [os](plugins/os) | Read information about the operating system. | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| [persisted-scope](plugins/persisted-scope) | Persist runtime scope changes on the filesystem. | ✅ | ✅ | ✅ | ? | ? |
|
| [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. | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "svelte-app",
|
"name": "api",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --clearScreen false",
|
"dev": "vite --clearScreen false",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"serve": "vite preview"
|
"serve": "vite preview",
|
||||||
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "2.0.3",
|
"@tauri-apps/api": "2.1.1",
|
||||||
"@tauri-apps/plugin-barcode-scanner": "2.0.0",
|
"@tauri-apps/plugin-barcode-scanner": "2.0.0",
|
||||||
"@tauri-apps/plugin-biometric": "2.0.0",
|
"@tauri-apps/plugin-biometric": "2.0.0",
|
||||||
"@tauri-apps/plugin-cli": "2.0.0",
|
"@tauri-apps/plugin-cli": "2.0.0",
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
"@tauri-apps/plugin-fs": "2.0.2",
|
"@tauri-apps/plugin-fs": "2.0.2",
|
||||||
"@tauri-apps/plugin-geolocation": "2.0.0",
|
"@tauri-apps/plugin-geolocation": "2.0.0",
|
||||||
"@tauri-apps/plugin-global-shortcut": "2.0.0",
|
"@tauri-apps/plugin-global-shortcut": "2.0.0",
|
||||||
|
"@tauri-apps/plugin-opener": "1.0.0",
|
||||||
"@tauri-apps/plugin-haptics": "2.0.0",
|
"@tauri-apps/plugin-haptics": "2.0.0",
|
||||||
"@tauri-apps/plugin-http": "2.0.1",
|
"@tauri-apps/plugin-http": "2.0.1",
|
||||||
"@tauri-apps/plugin-nfc": "2.0.0",
|
"@tauri-apps/plugin-nfc": "2.0.0",
|
||||||
@@ -33,7 +35,7 @@
|
|||||||
"@iconify-json/codicon": "^1.1.37",
|
"@iconify-json/codicon": "^1.1.37",
|
||||||
"@iconify-json/ph": "^1.1.8",
|
"@iconify-json/ph": "^1.1.8",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
"@tauri-apps/cli": "2.0.4",
|
"@tauri-apps/cli": "2.1.0",
|
||||||
"@unocss/extractor-svelte": "^0.64.0",
|
"@unocss/extractor-svelte": "^0.64.0",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"unocss": "^0.64.0",
|
"unocss": "^0.64.0",
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ tauri-plugin-notification = { path = "../../../plugins/notification", version =
|
|||||||
] }
|
] }
|
||||||
tauri-plugin-os = { path = "../../../plugins/os", version = "2.0.1" }
|
tauri-plugin-os = { path = "../../../plugins/os", version = "2.0.1" }
|
||||||
tauri-plugin-process = { path = "../../../plugins/process", version = "2.0.1" }
|
tauri-plugin-process = { path = "../../../plugins/process", version = "2.0.1" }
|
||||||
|
tauri-plugin-opener = { path = "../../../plugins/opener", version = "1.0.0" }
|
||||||
tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.0.2" }
|
tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.0.2" }
|
||||||
tauri-plugin-store = { path = "../../../plugins/store", version = "2.1.0" }
|
tauri-plugin-store = { path = "../../../plugins/store", version = "2.1.0" }
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,11 @@
|
|||||||
],
|
],
|
||||||
"deny": ["$APPDATA/db/*.stronghold"]
|
"deny": ["$APPDATA/db/*.stronghold"]
|
||||||
},
|
},
|
||||||
"store:default"
|
"store:default",
|
||||||
|
"opener:default",
|
||||||
|
{
|
||||||
|
"identifier": "opener:allow-open-path",
|
||||||
|
"allow": [{ "path": "$APPDATA" }, { "path": "$APPDATA/**" }]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,10 @@
|
|||||||
"geolocation:allow-check-permissions",
|
"geolocation:allow-check-permissions",
|
||||||
"geolocation:allow-request-permissions",
|
"geolocation:allow-request-permissions",
|
||||||
"geolocation:allow-watch-position",
|
"geolocation:allow-watch-position",
|
||||||
"geolocation:allow-get-current-position"
|
"geolocation:allow-get-current-position",
|
||||||
|
"haptics:allow-impact-feedback",
|
||||||
|
"haptics:allow-notification-feedback",
|
||||||
|
"haptics:allow-selection-feedback",
|
||||||
|
"haptics:allow-vibrate"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_notification::init())
|
.plugin(tauri_plugin_notification::init())
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_store::Builder::default().build())
|
.plugin(tauri_plugin_store::Builder::default().build())
|
||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { writable } from 'svelte/store'
|
import { writable } from 'svelte/store'
|
||||||
import { open } from '@tauri-apps/plugin-shell'
|
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||||
import * as os from '@tauri-apps/plugin-os'
|
import * as os from '@tauri-apps/plugin-os'
|
||||||
@@ -14,6 +13,7 @@
|
|||||||
import Notifications from './views/Notifications.svelte'
|
import Notifications from './views/Notifications.svelte'
|
||||||
import Shortcuts from './views/Shortcuts.svelte'
|
import Shortcuts from './views/Shortcuts.svelte'
|
||||||
import Shell from './views/Shell.svelte'
|
import Shell from './views/Shell.svelte'
|
||||||
|
import Opener from './views/Opener.svelte'
|
||||||
import Store from './views/Store.svelte'
|
import Store from './views/Store.svelte'
|
||||||
import Updater from './views/Updater.svelte'
|
import Updater from './views/Updater.svelte'
|
||||||
import Clipboard from './views/Clipboard.svelte'
|
import Clipboard from './views/Clipboard.svelte'
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
import Scanner from './views/Scanner.svelte'
|
import Scanner from './views/Scanner.svelte'
|
||||||
import Biometric from './views/Biometric.svelte'
|
import Biometric from './views/Biometric.svelte'
|
||||||
import Geolocation from './views/Geolocation.svelte'
|
import Geolocation from './views/Geolocation.svelte'
|
||||||
|
import Haptics from './views/Haptics.svelte'
|
||||||
|
|
||||||
import { onMount, tick } from 'svelte'
|
import { onMount, tick } from 'svelte'
|
||||||
import { ask } from '@tauri-apps/plugin-dialog'
|
import { ask } from '@tauri-apps/plugin-dialog'
|
||||||
@@ -91,6 +92,11 @@
|
|||||||
component: Shell,
|
component: Shell,
|
||||||
icon: 'i-codicon-terminal-bash'
|
icon: 'i-codicon-terminal-bash'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Opener',
|
||||||
|
component: Opener,
|
||||||
|
icon: 'i-codicon-link-external'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Store',
|
label: 'Store',
|
||||||
component: Store,
|
component: Store,
|
||||||
@@ -130,6 +136,11 @@
|
|||||||
label: 'Geolocation',
|
label: 'Geolocation',
|
||||||
component: Geolocation,
|
component: Geolocation,
|
||||||
icon: 'i-ph-map-pin'
|
icon: 'i-ph-map-pin'
|
||||||
|
},
|
||||||
|
isMobile && {
|
||||||
|
label: 'Haptics',
|
||||||
|
component: Haptics,
|
||||||
|
icon: 'i-ph-vibrate'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script>
|
||||||
|
import {
|
||||||
|
vibrate,
|
||||||
|
impactFeedback,
|
||||||
|
notificationFeedback,
|
||||||
|
selectionFeedback
|
||||||
|
} from '@tauri-apps/plugin-haptics'
|
||||||
|
|
||||||
|
export let onMessage
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
on:click={() => vibrate(300).then(onMessage).catch(onMessage)}
|
||||||
|
>vibrate short</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
on:click={() => vibrate(1500).then(onMessage).catch(onMessage)}
|
||||||
|
>vibrate long</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
on:click={() => impactFeedback('medium').then(onMessage).catch(onMessage)}
|
||||||
|
>impact medium</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
on:click={() =>
|
||||||
|
notificationFeedback('warning').then(onMessage).catch(onMessage)}
|
||||||
|
>notification warning</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
on:click={() => selectionFeedback().then(onMessage).catch(onMessage)}
|
||||||
|
>selection</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Depending on your device settings for haptic feedback some of the buttons may
|
||||||
|
not work.
|
||||||
|
</p>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<script>
|
||||||
|
import * as opener from '@tauri-apps/plugin-opener'
|
||||||
|
|
||||||
|
export let onMessage
|
||||||
|
|
||||||
|
let url = ''
|
||||||
|
let urlProgram = ''
|
||||||
|
function openUrl() {
|
||||||
|
opener.openUrl(url, urlProgram ? urlProgram : undefined).catch(onMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = ''
|
||||||
|
let pathProgram = ''
|
||||||
|
function openPath() {
|
||||||
|
opener
|
||||||
|
.openPath(path, pathProgram ? pathProgram : undefined)
|
||||||
|
.catch(onMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
let revealPath = ''
|
||||||
|
function revealItemInDir() {
|
||||||
|
opener.revealItemInDir(revealPath).catch(onMessage)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<form
|
||||||
|
class="flex flex-row gap-2 items-center"
|
||||||
|
on:submit|preventDefault={openUrl}
|
||||||
|
>
|
||||||
|
<button class="btn" type="submit">Open URL</button>
|
||||||
|
<input
|
||||||
|
class="input grow"
|
||||||
|
placeholder="Type the URL to open..."
|
||||||
|
bind:value={url}
|
||||||
|
/>
|
||||||
|
<span> with </span>
|
||||||
|
<input class="input" bind:value={urlProgram} />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="flex flex-row gap-2 items-center"
|
||||||
|
on:submit|preventDefault={openPath}
|
||||||
|
>
|
||||||
|
<button class="btn" type="submit">Open Path</button>
|
||||||
|
<input
|
||||||
|
class="input grow"
|
||||||
|
placeholder="Type the path to open..."
|
||||||
|
bind:value={path}
|
||||||
|
/>
|
||||||
|
<span> with </span>
|
||||||
|
<input class="input" bind:value={pathProgram} />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="flex flex-row gap-2 items-center"
|
||||||
|
on:submit|preventDefault={revealItemInDir}
|
||||||
|
>
|
||||||
|
<button class="btn" type="submit">Reveal</button>
|
||||||
|
<input
|
||||||
|
class="input grow"
|
||||||
|
placeholder="Type the path to reveal..."
|
||||||
|
bind:value={revealPath}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
+8
-7
@@ -7,23 +7,24 @@
|
|||||||
"build": "pnpm run -r --parallel --filter !plugins-workspace --filter !\"./plugins/*/examples/**\" --filter !\"./examples/*\" build",
|
"build": "pnpm run -r --parallel --filter !plugins-workspace --filter !\"./plugins/*/examples/**\" --filter !\"./examples/*\" build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check ."
|
"format:check": "prettier --check .",
|
||||||
|
"example:api:dev": "pnpm run --filter \"api\" tauri dev"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "9.14.0",
|
"@eslint/js": "9.15.0",
|
||||||
"@rollup/plugin-node-resolve": "15.3.0",
|
"@rollup/plugin-node-resolve": "15.3.0",
|
||||||
"@rollup/plugin-terser": "0.4.4",
|
"@rollup/plugin-terser": "0.4.4",
|
||||||
"@rollup/plugin-typescript": "11.1.6",
|
"@rollup/plugin-typescript": "11.1.6",
|
||||||
"@types/eslint__js": "8.42.3",
|
"@types/eslint__js": "8.42.3",
|
||||||
"covector": "^0.12.3",
|
"covector": "^0.12.3",
|
||||||
"eslint": "9.14.0",
|
"eslint": "9.15.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
"eslint-plugin-security": "3.0.1",
|
"eslint-plugin-security": "3.0.1",
|
||||||
"prettier": "3.3.3",
|
"prettier": "3.4.0",
|
||||||
"rollup": "4.24.4",
|
"rollup": "4.27.4",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"typescript": "5.6.3",
|
"typescript": "5.7.2",
|
||||||
"typescript-eslint": "8.13.0"
|
"typescript-eslint": "8.16.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"semver": ">=7.5.2",
|
"semver": ">=7.5.2",
|
||||||
|
|||||||
@@ -27,6 +27,5 @@ tauri-plugin = { workspace = true, features = ["build"] }
|
|||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tauri = { workspace = true }
|
tauri = { workspace = true }
|
||||||
log = { workspace = true }
|
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
auto-launch = "0.5"
|
auto-launch = "0.5"
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//! [](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/autostart)
|
|
||||||
//!
|
|
||||||
//! Automatically launch your application at startup. Supports Windows, Mac (via AppleScript or Launch Agent), and Linux.
|
//! Automatically launch your application at startup. Supports Windows, Mac (via AppleScript or Launch Agent), and Linux.
|
||||||
|
|
||||||
#![doc(
|
#![doc(
|
||||||
@@ -13,8 +11,6 @@
|
|||||||
#![cfg(not(any(target_os = "android", target_os = "ios")))]
|
#![cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
|
||||||
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
|
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use log::info;
|
|
||||||
use serde::{ser::Serializer, Serialize};
|
use serde::{ser::Serializer, Serialize};
|
||||||
use tauri::{
|
use tauri::{
|
||||||
command,
|
command,
|
||||||
@@ -135,7 +131,6 @@ pub fn init<R: Runtime>(
|
|||||||
} else {
|
} else {
|
||||||
exe_path
|
exe_path
|
||||||
};
|
};
|
||||||
info!("auto_start path {}", &app_path);
|
|
||||||
builder.set_app_path(&app_path);
|
builder.set_app_path(&app_path);
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//! [](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/cli)
|
|
||||||
//!
|
|
||||||
//! Parse arguments from your Command Line Interface.
|
//! Parse arguments from your Command Line Interface.
|
||||||
//!
|
//!
|
||||||
//! - Supported platforms: Windows, Linux and macOS.
|
//! - Supported platforms: Windows, Linux and macOS.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
if("__TAURI__"in window){var __TAURI_PLUGIN_CLIPBOARD_MANAGER__=function(e){"use strict";var t;async function r(e,t={},r){return window.__TAURI_INTERNALS__.invoke(e,t,r)}"function"==typeof SuppressedError&&SuppressedError;class n{get rid(){return function(e,t,r,n){if("a"===r&&!n)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!n:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?n:"a"===r?n.call(e):n?n.value:t.get(e)}(this,t,"f")}constructor(e){t.set(this,void 0),function(e,t,r,n,a){if("function"==typeof t?e!==t||!a:!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 r("plugin:resources|close",{rid:this.rid})}}t=new WeakMap;class a extends n{constructor(e){super(e)}static async new(e,t,n){return r("plugin:image|new",{rgba:i(e),width:t,height:n}).then((e=>new a(e)))}static async fromBytes(e){return r("plugin:image|from_bytes",{bytes:i(e)}).then((e=>new a(e)))}static async fromPath(e){return r("plugin:image|from_path",{path:e}).then((e=>new a(e)))}async rgba(){return r("plugin:image|rgba",{rid:this.rid}).then((e=>new Uint8Array(e)))}async size(){return r("plugin:image|size",{rid:this.rid})}}function i(e){return null==e?null:"string"==typeof e?e:e instanceof a?e.rid:e}return e.clear=async function(){await r("plugin:clipboard-manager|clear")},e.readImage=async function(){return await r("plugin:clipboard-manager|read_image").then((e=>new a(e)))},e.readText=async function(){return await r("plugin:clipboard-manager|read_text")},e.writeHtml=async function(e,t){await r("plugin:clipboard-manager|write_html",{html:e,altHtml:t})},e.writeImage=async function(e){await r("plugin:clipboard-manager|write_image",{image:i(e)})},e.writeText=async function(e,t){await r("plugin:clipboard-manager|write_text",{label:t?.label,text:e})},e}({});Object.defineProperty(window.__TAURI__,"clipboardManager",{value:__TAURI_PLUGIN_CLIPBOARD_MANAGER__})}
|
if("__TAURI__"in window){var __TAURI_PLUGIN_CLIPBOARD_MANAGER__=function(e){"use strict";var t;async function r(e,t={},r){return window.__TAURI_INTERNALS__.invoke(e,t,r)}"function"==typeof SuppressedError&&SuppressedError;class n{get rid(){return function(e,t,r,n){if("a"===r&&!n)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!n:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?n:"a"===r?n.call(e):n?n.value:t.get(e)}(this,t,"f")}constructor(e){t.set(this,void 0),function(e,t,r,n,a){if("function"==typeof t?e!==t||!a:!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 r("plugin:resources|close",{rid:this.rid})}}t=new WeakMap;class a extends n{constructor(e){super(e)}static async new(e,t,n){return r("plugin:image|new",{rgba:i(e),width:t,height:n}).then((e=>new a(e)))}static async fromBytes(e){return r("plugin:image|from_bytes",{bytes:i(e)}).then((e=>new a(e)))}static async fromPath(e){return r("plugin:image|from_path",{path:e}).then((e=>new a(e)))}async rgba(){return r("plugin:image|rgba",{rid:this.rid}).then((e=>new Uint8Array(e)))}async size(){return r("plugin:image|size",{rid:this.rid})}}function i(e){return null==e?null:"string"==typeof e?e:e instanceof a?e.rid:e}return e.clear=async function(){await r("plugin:clipboard-manager|clear")},e.readImage=async function(){return await r("plugin:clipboard-manager|read_image").then((e=>new a(e)))},e.readText=async function(){return await r("plugin:clipboard-manager|read_text")},e.writeHtml=async function(e,t){await r("plugin:clipboard-manager|write_html",{html:e,altText:t})},e.writeImage=async function(e){await r("plugin:clipboard-manager|write_image",{image:i(e)})},e.writeText=async function(e,t){await r("plugin:clipboard-manager|write_text",{label:t?.label,text:e})},e}({});Object.defineProperty(window.__TAURI__,"clipboardManager",{value:__TAURI_PLUGIN_CLIPBOARD_MANAGER__})}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ async function writeImage(
|
|||||||
* import { readImage } from '@tauri-apps/plugin-clipboard-manager';
|
* import { readImage } from '@tauri-apps/plugin-clipboard-manager';
|
||||||
*
|
*
|
||||||
* const clipboardImage = await readImage();
|
* const clipboardImage = await readImage();
|
||||||
* const blob = new Blob([clipboardImage.bytes], { type: 'image' })
|
* const blob = new Blob([await clipboardImage.rbga()], { type: 'image' })
|
||||||
* const url = URL.createObjectURL(blob)
|
* const url = URL.createObjectURL(blob)
|
||||||
* ```
|
* ```
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
@@ -120,10 +120,10 @@ async function readImage(): Promise<Image> {
|
|||||||
*
|
*
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
async function writeHtml(html: string, altHtml?: string): Promise<void> {
|
async function writeHtml(html: string, altText?: string): Promise<void> {
|
||||||
await invoke('plugin:clipboard-manager|write_html', {
|
await invoke('plugin:clipboard-manager|write_html', {
|
||||||
html,
|
html,
|
||||||
altHtml
|
altText
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//! [](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/clipboard-manager)
|
|
||||||
//!
|
|
||||||
//! Read and write to the system clipboard.
|
//! Read and write to the system clipboard.
|
||||||
|
|
||||||
#![doc(
|
#![doc(
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ serde = { workspace = true }
|
|||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tauri = { workspace = true }
|
tauri = { workspace = true }
|
||||||
tauri-utils = { workspace = true }
|
tauri-utils = { workspace = true }
|
||||||
log = { workspace = true }
|
tracing = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
if("__TAURI__"in window){var __TAURI_PLUGIN_DEEP_LINK__=function(e){"use strict";function n(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}async function r(e,n={},r){return window.__TAURI_INTERNALS__.invoke(e,n,r)}var t;async function i(e,t,i){const a={kind:"Any"};return r("plugin:event|listen",{event:e,target:a,handler:n(t)}).then((n=>async()=>async function(e,n){await r("plugin:event|unlisten",{event:e,eventId:n})}(e,n)))}async function a(){return await r("plugin:deep-link|get_current")}return"function"==typeof SuppressedError&&SuppressedError,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG_ENTER="tauri://drag-enter",e.DRAG_OVER="tauri://drag-over",e.DRAG_DROP="tauri://drag-drop",e.DRAG_LEAVE="tauri://drag-leave"}(t||(t={})),e.getCurrent=a,e.isRegistered=async function(e){return await r("plugin:deep-link|is_registered",{protocol:e})},e.onOpenUrl=async function(e){const n=await a();return n&&e(n),await i("deep-link://new-url",(n=>{e(n.payload)}))},e.register=async function(e){return await r("plugin:deep-link|register",{protocol:e})},e.unregister=async function(e){return await r("plugin:deep-link|unregister",{protocol:e})},e}({});Object.defineProperty(window.__TAURI__,"deepLink",{value:__TAURI_PLUGIN_DEEP_LINK__})}
|
if("__TAURI__"in window){var __TAURI_PLUGIN_DEEP_LINK__=function(e){"use strict";function n(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}async function r(e,n={},r){return window.__TAURI_INTERNALS__.invoke(e,n,r)}var t;async function i(e,t,i){const a={kind:"Any"};return r("plugin:event|listen",{event:e,target:a,handler:n(t)}).then((n=>async()=>async function(e,n){await r("plugin:event|unlisten",{event:e,eventId:n})}(e,n)))}return"function"==typeof SuppressedError&&SuppressedError,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG_ENTER="tauri://drag-enter",e.DRAG_OVER="tauri://drag-over",e.DRAG_DROP="tauri://drag-drop",e.DRAG_LEAVE="tauri://drag-leave"}(t||(t={})),e.getCurrent=async function(){return await r("plugin:deep-link|get_current")},e.isRegistered=async function(e){return await r("plugin:deep-link|is_registered",{protocol:e})},e.onOpenUrl=async function(e){return await i("deep-link://new-url",(n=>{e(n.payload)}))},e.register=async function(e){return await r("plugin:deep-link|register",{protocol:e})},e.unregister=async function(e){return await r("plugin:deep-link|unregister",{protocol:e})},e}({});Object.defineProperty(window.__TAURI__,"deepLink",{value:__TAURI_PLUGIN_DEEP_LINK__})}
|
||||||
|
|||||||
@@ -10,11 +10,11 @@
|
|||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "2.0.3",
|
"@tauri-apps/api": "2.1.1",
|
||||||
"@tauri-apps/plugin-deep-link": "2.0.0"
|
"@tauri-apps/plugin-deep-link": "2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "2.0.4",
|
"@tauri-apps/cli": "2.1.0",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.4.7"
|
"vite": "^5.4.7"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export async function unregister(protocol: string): Promise<null> {
|
|||||||
* await isRegistered("my-scheme");
|
* await isRegistered("my-scheme");
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* #### - **macOS / Android / iOS**: Unsupported, always returns `true`.
|
* #### - **macOS / Android / iOS**: Unsupported.
|
||||||
*
|
*
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
@@ -92,18 +92,13 @@ export async function isRegistered(protocol: string): Promise<boolean> {
|
|||||||
* await onOpenUrl((urls) => { console.log(urls) });
|
* await onOpenUrl((urls) => { console.log(urls) });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* #### - **Windows / Linux**: Unsupported, the OS will spawn a new app instance passing the URL as a CLI argument.
|
* #### - **Windows / Linux**: Unsupported without the single-instance plugin. The OS will spawn a new app instance passing the URL as a CLI argument.
|
||||||
*
|
*
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
export async function onOpenUrl(
|
export async function onOpenUrl(
|
||||||
handler: (urls: string[]) => void
|
handler: (urls: string[]) => void
|
||||||
): Promise<UnlistenFn> {
|
): Promise<UnlistenFn> {
|
||||||
const current = await getCurrent()
|
|
||||||
if (current) {
|
|
||||||
handler(current)
|
|
||||||
}
|
|
||||||
|
|
||||||
return await listen<string[]>('deep-link://new-url', (event) => {
|
return await listen<string[]>('deep-link://new-url', (event) => {
|
||||||
handler(event.payload)
|
handler(event.payload)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use tauri::{
|
use tauri::{
|
||||||
plugin::{Builder, PluginApi, TauriPlugin},
|
plugin::{Builder, PluginApi, TauriPlugin},
|
||||||
AppHandle, EventId, Listener, Manager, Runtime,
|
AppHandle, EventId, Listener, Manager, Runtime,
|
||||||
@@ -217,7 +215,7 @@ mod imp {
|
|||||||
current.replace(vec![url.clone()]);
|
current.replace(vec![url.clone()]);
|
||||||
let _ = self.app.emit("deep-link://new-url", vec![url]);
|
let _ = self.app.emit("deep-link://new-url", vec![url]);
|
||||||
} else if cfg!(debug_assertions) {
|
} else if cfg!(debug_assertions) {
|
||||||
log::warn!("argument {url} does not match any configured deep link scheme; skipping it");
|
tracing::warn!("argument {url} does not match any configured deep link scheme; skipping it");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -478,13 +476,10 @@ impl OpenUrlEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Runtime> DeepLink<R> {
|
impl<R: Runtime> DeepLink<R> {
|
||||||
/// Handle a new deep link being triggered to open the app.
|
/// Helper function for the `deep-link://new-url` event to run a function each time the protocol is triggered while the app is running.
|
||||||
///
|
///
|
||||||
/// To avoid race conditions, if the app was started with a deep link,
|
/// Use `get_current` on app load to check whether your app was started via a deep link.
|
||||||
/// the closure gets immediately called with the deep link URL.
|
|
||||||
pub fn on_open_url<F: Fn(OpenUrlEvent) + Send + Sync + 'static>(&self, f: F) -> EventId {
|
pub fn on_open_url<F: Fn(OpenUrlEvent) + Send + Sync + 'static>(&self, f: F) -> EventId {
|
||||||
let f = Arc::new(f);
|
|
||||||
let f_ = f.clone();
|
|
||||||
let event_id = self.app.listen("deep-link://new-url", move |event| {
|
let event_id = self.app.listen("deep-link://new-url", move |event| {
|
||||||
if let Ok(urls) = serde_json::from_str(event.payload()) {
|
if let Ok(urls) = serde_json::from_str(event.payload()) {
|
||||||
f(OpenUrlEvent {
|
f(OpenUrlEvent {
|
||||||
@@ -494,13 +489,6 @@ impl<R: Runtime> DeepLink<R> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Ok(Some(current)) = self.get_current() {
|
|
||||||
f_(OpenUrlEvent {
|
|
||||||
id: event_id,
|
|
||||||
urls: current,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
event_id
|
event_id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ pub(crate) async fn open<R: Runtime>(
|
|||||||
for folder in folders {
|
for folder in folders {
|
||||||
if let Ok(path) = folder.clone().into_path() {
|
if let Ok(path) = folder.clone().into_path() {
|
||||||
if let Some(s) = window.try_fs_scope() {
|
if let Some(s) = window.try_fs_scope() {
|
||||||
s.allow_directory(&path, options.recursive);
|
s.allow_directory(&path, options.recursive)?;
|
||||||
}
|
}
|
||||||
tauri_scope.allow_directory(&path, options.directory)?;
|
tauri_scope.allow_directory(&path, options.directory)?;
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ pub(crate) async fn open<R: Runtime>(
|
|||||||
if let Some(folder) = &folder {
|
if let Some(folder) = &folder {
|
||||||
if let Ok(path) = folder.clone().into_path() {
|
if let Ok(path) = folder.clone().into_path() {
|
||||||
if let Some(s) = window.try_fs_scope() {
|
if let Some(s) = window.try_fs_scope() {
|
||||||
s.allow_directory(&path, options.recursive);
|
s.allow_directory(&path, options.recursive)?;
|
||||||
}
|
}
|
||||||
tauri_scope.allow_directory(&path, options.directory)?;
|
tauri_scope.allow_directory(&path, options.directory)?;
|
||||||
}
|
}
|
||||||
@@ -175,7 +175,7 @@ pub(crate) async fn open<R: Runtime>(
|
|||||||
for file in files {
|
for file in files {
|
||||||
if let Ok(path) = file.clone().into_path() {
|
if let Ok(path) = file.clone().into_path() {
|
||||||
if let Some(s) = window.try_fs_scope() {
|
if let Some(s) = window.try_fs_scope() {
|
||||||
s.allow_file(&path);
|
s.allow_file(&path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
tauri_scope.allow_file(&path)?;
|
tauri_scope.allow_file(&path)?;
|
||||||
@@ -190,7 +190,7 @@ pub(crate) async fn open<R: Runtime>(
|
|||||||
if let Some(file) = &file {
|
if let Some(file) = &file {
|
||||||
if let Ok(path) = file.clone().into_path() {
|
if let Ok(path) = file.clone().into_path() {
|
||||||
if let Some(s) = window.try_fs_scope() {
|
if let Some(s) = window.try_fs_scope() {
|
||||||
s.allow_file(&path);
|
s.allow_file(&path)?;
|
||||||
}
|
}
|
||||||
tauri_scope.allow_file(&path)?;
|
tauri_scope.allow_file(&path)?;
|
||||||
}
|
}
|
||||||
@@ -232,7 +232,7 @@ pub(crate) async fn save<R: Runtime>(
|
|||||||
if let Some(p) = &path {
|
if let Some(p) = &path {
|
||||||
if let Ok(path) = p.clone().into_path() {
|
if let Ok(path) = p.clone().into_path() {
|
||||||
if let Some(s) = window.try_fs_scope() {
|
if let Some(s) = window.try_fs_scope() {
|
||||||
s.allow_file(&path);
|
s.allow_file(&path)?;
|
||||||
}
|
}
|
||||||
tauri_scope.allow_file(&path)?;
|
tauri_scope.allow_file(&path)?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//! [](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/dialog)
|
|
||||||
//!
|
|
||||||
//! Native system dialogs for opening and saving files along with message dialogs.
|
//! Native system dialogs for opening and saving files along with message dialogs.
|
||||||
|
|
||||||
#![doc(
|
#![doc(
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ rustc-args = ["--cfg", "docsrs"]
|
|||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
[package.metadata.platforms.support]
|
[package.metadata.platforms.support]
|
||||||
windows = { level = "full", notes = "" }
|
windows = { level = "full", notes = "Apps installed via MSI or NSIS in `perMachine` and `both` mode require admin permissions for write acces in `$RESOURCES` folder" }
|
||||||
linux = { level = "full", notes = "No write access to `$RESOURCES` folder" }
|
linux = { level = "full", notes = "No write access to `$RESOURCES` folder" }
|
||||||
macos = { level = "full", notes = "No write access to `$RESOURCES` folder" }
|
macos = { level = "full", notes = "No write access to `$RESOURCES` folder" }
|
||||||
android = { level = "partial", notes = "Access is restricted to Application folder by default" }
|
android = { level = "partial", notes = "Access is restricted to Application folder by default" }
|
||||||
@@ -24,6 +24,8 @@ ios = { level = "partial", notes = "Access is restricted to Application folder b
|
|||||||
tauri-plugin = { workspace = true, features = ["build"] }
|
tauri-plugin = { workspace = true, features = ["build"] }
|
||||||
schemars = { workspace = true }
|
schemars = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
toml = "0.8"
|
||||||
|
tauri-utils = { workspace = true, features = ["build"] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
@@ -34,7 +36,7 @@ thiserror = { workspace = true }
|
|||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
glob = "0.3"
|
glob = { workspace = true }
|
||||||
# TODO: Remove `serialization-compat-6` in v3
|
# TODO: Remove `serialization-compat-6` in v3
|
||||||
notify = { version = "7", optional = true, features = [
|
notify = { version = "7", optional = true, features = [
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+82
-28
@@ -7,6 +7,8 @@ use std::{
|
|||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use tauri_utils::acl::manifest::PermissionFile;
|
||||||
|
|
||||||
#[path = "src/scope.rs"]
|
#[path = "src/scope.rs"]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
mod scope;
|
mod scope;
|
||||||
@@ -16,10 +18,23 @@ mod scope;
|
|||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
enum FsScopeEntry {
|
enum FsScopeEntry {
|
||||||
/// FS scope path.
|
/// A path that can be accessed by the webview when using the fs APIs.
|
||||||
|
/// FS scope path pattern.
|
||||||
|
///
|
||||||
|
/// The pattern can start with a variable that resolves to a system base directory.
|
||||||
|
/// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,
|
||||||
|
/// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,
|
||||||
|
/// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,
|
||||||
|
/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
|
||||||
Value(PathBuf),
|
Value(PathBuf),
|
||||||
Object {
|
Object {
|
||||||
/// FS scope path.
|
/// A path that can be accessed by the webview when using the fs APIs.
|
||||||
|
///
|
||||||
|
/// The pattern can start with a variable that resolves to a system base directory.
|
||||||
|
/// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,
|
||||||
|
/// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,
|
||||||
|
/// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,
|
||||||
|
/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -62,31 +77,32 @@ const BASE_DIR_VARS: &[&str] = &[
|
|||||||
"APPCACHE",
|
"APPCACHE",
|
||||||
"APPLOG",
|
"APPLOG",
|
||||||
];
|
];
|
||||||
const COMMANDS: &[&str] = &[
|
const COMMANDS: &[(&str, &[&str])] = &[
|
||||||
"mkdir",
|
("mkdir", &[]),
|
||||||
"create",
|
("create", &[]),
|
||||||
"copy_file",
|
("copy_file", &[]),
|
||||||
"remove",
|
("remove", &[]),
|
||||||
"rename",
|
("rename", &[]),
|
||||||
"truncate",
|
("truncate", &[]),
|
||||||
"ftruncate",
|
("ftruncate", &[]),
|
||||||
"write",
|
("write", &[]),
|
||||||
"write_file",
|
("write_file", &["open", "write"]),
|
||||||
"write_text_file",
|
("write_text_file", &[]),
|
||||||
"read_dir",
|
("read_dir", &[]),
|
||||||
"read_file",
|
("read_file", &[]),
|
||||||
"read",
|
("read", &[]),
|
||||||
"open",
|
("open", &[]),
|
||||||
"read_text_file",
|
("read_text_file", &[]),
|
||||||
"read_text_file_lines",
|
("read_text_file_lines", &["read_text_file_lines_next"]),
|
||||||
"read_text_file_lines_next",
|
("read_text_file_lines_next", &[]),
|
||||||
"seek",
|
("seek", &[]),
|
||||||
"stat",
|
("stat", &[]),
|
||||||
"lstat",
|
("lstat", &[]),
|
||||||
"fstat",
|
("fstat", &[]),
|
||||||
"exists",
|
("exists", &[]),
|
||||||
"watch",
|
("watch", &[]),
|
||||||
"unwatch",
|
("unwatch", &[]),
|
||||||
|
("size", &[]),
|
||||||
];
|
];
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -192,9 +208,47 @@ permissions = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tauri_plugin::Builder::new(COMMANDS)
|
tauri_plugin::Builder::new(&COMMANDS.iter().map(|c| c.0).collect::<Vec<_>>())
|
||||||
.global_api_script_path("./api-iife.js")
|
.global_api_script_path("./api-iife.js")
|
||||||
.global_scope_schema(schemars::schema_for!(FsScopeEntry))
|
.global_scope_schema(schemars::schema_for!(FsScopeEntry))
|
||||||
.android_path("android")
|
.android_path("android")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
// workaround to include nested permissions as `tauri_plugin` doesn't support it
|
||||||
|
let permissions_dir = autogenerated.join("commands");
|
||||||
|
for (command, nested_commands) in COMMANDS {
|
||||||
|
if nested_commands.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let permission_path = permissions_dir.join(format!("{command}.toml"));
|
||||||
|
|
||||||
|
let content = std::fs::read_to_string(&permission_path)
|
||||||
|
.unwrap_or_else(|_| panic!("failed to read {command}.toml"));
|
||||||
|
|
||||||
|
let mut permission_file = toml::from_str::<PermissionFile>(&content)
|
||||||
|
.unwrap_or_else(|_| panic!("failed to deserialize {command}.toml"));
|
||||||
|
|
||||||
|
for p in permission_file
|
||||||
|
.permission
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|p| p.identifier.starts_with("allow"))
|
||||||
|
{
|
||||||
|
p.commands
|
||||||
|
.allow
|
||||||
|
.extend(nested_commands.iter().map(|s| s.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let out = toml::to_string_pretty(&permission_file)
|
||||||
|
.unwrap_or_else(|_| panic!("failed to serialize {command}.toml"));
|
||||||
|
let out = format!(
|
||||||
|
r#"# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
{out}"#
|
||||||
|
);
|
||||||
|
std::fs::write(permission_path, out)
|
||||||
|
.unwrap_or_else(|_| panic!("failed to write {command}.toml"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,16 +14,29 @@
|
|||||||
*
|
*
|
||||||
* The API has a scope configuration that forces you to restrict the paths that can be accessed using glob patterns.
|
* The API has a scope configuration that forces you to restrict the paths that can be accessed using glob patterns.
|
||||||
*
|
*
|
||||||
* The scope configuration is an array of glob patterns describing folder paths that are allowed.
|
* The scope configuration is an array of glob patterns describing file/directory paths that are allowed.
|
||||||
* For instance, this scope configuration only allows accessing files on the
|
* For instance, this scope configuration allows **all** enabled `fs` APIs to (only) access files in the
|
||||||
* *databases* folder of the {@link https://v2.tauri.app/reference/javascript/api/namespacepath/#appdatadir | `$APPDATA` directory}:
|
* *databases* directory of the {@link https://v2.tauri.app/reference/javascript/api/namespacepath/#appdatadir | `$APPDATA` directory}:
|
||||||
* ```json
|
* ```json
|
||||||
* {
|
* {
|
||||||
* "plugins": {
|
* "permissions": [
|
||||||
* "fs": {
|
* {
|
||||||
* "scope": ["$APPDATA/databases/*"]
|
* "identifier": "fs:scope",
|
||||||
|
* "allow": [{ "path": "$APPDATA/databases/*" }]
|
||||||
* }
|
* }
|
||||||
* }
|
* ]
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Scopes can also be applied to specific `fs` APIs by using the API's identifier instead of `fs:scope`:
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "permissions": [
|
||||||
|
* {
|
||||||
|
* "identifier": "fs:allow-exists",
|
||||||
|
* "allow": [{ "path": "$APPDATA/databases/*" }]
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
@@ -56,8 +69,6 @@
|
|||||||
*
|
*
|
||||||
* Trying to execute any API with a URL not configured on the scope results in a promise rejection due to denied access.
|
* Trying to execute any API with a URL not configured on the scope results in a promise rejection due to denied access.
|
||||||
*
|
*
|
||||||
* Note that this scope applies to **all** APIs on this module.
|
|
||||||
*
|
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -255,6 +266,7 @@ function fromBytes(buffer: FixedSizeArray<number, 8>): number {
|
|||||||
const size = bytes.byteLength
|
const size = bytes.byteLength
|
||||||
let x = 0
|
let x = 0
|
||||||
for (let i = 0; i < size; i++) {
|
for (let i = 0; i < size; i++) {
|
||||||
|
// eslint-disable-next-line security/detect-object-injection
|
||||||
const byte = bytes[i]
|
const byte = bytes[i]
|
||||||
x *= 0x100
|
x *= 0x100
|
||||||
x += byte
|
x += byte
|
||||||
@@ -416,11 +428,11 @@ class FileHandle extends Resource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes `p.byteLength` bytes from `p` to the underlying data stream. It
|
* Writes `data.byteLength` bytes from `data` to the underlying data stream. It
|
||||||
* resolves to the number of bytes written from `p` (`0` <= `n` <=
|
* resolves to the number of bytes written from `data` (`0` <= `n` <=
|
||||||
* `p.byteLength`) or reject with the error encountered that caused the
|
* `data.byteLength`) or reject with the error encountered that caused the
|
||||||
* write to stop early. `write()` must reject with a non-null error if
|
* write to stop early. `write()` must reject with a non-null error if
|
||||||
* would resolve to `n` < `p.byteLength`. `write()` must not modify the
|
* would resolve to `n` < `data.byteLength`. `write()` must not modify the
|
||||||
* slice data, even temporarily.
|
* slice data, even temporarily.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
@@ -758,10 +770,14 @@ async function readTextFile(
|
|||||||
throw new TypeError('Must be a file URL.')
|
throw new TypeError('Must be a file URL.')
|
||||||
}
|
}
|
||||||
|
|
||||||
return await invoke<string>('plugin:fs|read_text_file', {
|
const arr = await invoke<ArrayBuffer | number[]>('plugin:fs|read_text_file', {
|
||||||
path: path instanceof URL ? path.toString() : path,
|
path: path instanceof URL ? path.toString() : path,
|
||||||
options
|
options
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const bytes = arr instanceof ArrayBuffer ? arr : Uint8Array.from(arr)
|
||||||
|
|
||||||
|
return new TextDecoder().decode(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -792,6 +808,7 @@ async function readTextFileLines(
|
|||||||
return await Promise.resolve({
|
return await Promise.resolve({
|
||||||
path: pathStr,
|
path: pathStr,
|
||||||
rid: null as number | null,
|
rid: null as number | null,
|
||||||
|
|
||||||
async next(): Promise<IteratorResult<string>> {
|
async next(): Promise<IteratorResult<string>> {
|
||||||
if (this.rid === null) {
|
if (this.rid === null) {
|
||||||
this.rid = await invoke<number>('plugin:fs|read_text_file_lines', {
|
this.rid = await invoke<number>('plugin:fs|read_text_file_lines', {
|
||||||
@@ -800,19 +817,35 @@ async function readTextFileLines(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const [line, done] = await invoke<[string | null, boolean]>(
|
const arr = await invoke<ArrayBuffer | number[]>(
|
||||||
'plugin:fs|read_text_file_lines_next',
|
'plugin:fs|read_text_file_lines_next',
|
||||||
{ rid: this.rid }
|
{ rid: this.rid }
|
||||||
)
|
)
|
||||||
|
|
||||||
// an iteration is over, reset rid for next iteration
|
const bytes =
|
||||||
if (done) this.rid = null
|
arr instanceof ArrayBuffer ? new Uint8Array(arr) : Uint8Array.from(arr)
|
||||||
|
|
||||||
|
// Rust side will never return an empty array for this command and
|
||||||
|
// ensure there is at least one elements there.
|
||||||
|
//
|
||||||
|
// This is an optimization to include whether we finished iteration or not (1 or 0)
|
||||||
|
// at the end of returned array to avoid serialization overhead of separate values.
|
||||||
|
const done = bytes[bytes.byteLength - 1] === 1
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
// a full iteration is over, reset rid for next iteration
|
||||||
|
this.rid = null
|
||||||
|
return { value: null, done }
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = new TextDecoder().decode(bytes.slice(0, bytes.byteLength))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: done ? '' : line!,
|
value: line,
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
[Symbol.asyncIterator](): AsyncIterableIterator<string> {
|
[Symbol.asyncIterator](): AsyncIterableIterator<string> {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
@@ -1033,19 +1066,27 @@ interface WriteFileOptions {
|
|||||||
*/
|
*/
|
||||||
async function writeFile(
|
async function writeFile(
|
||||||
path: string | URL,
|
path: string | URL,
|
||||||
data: Uint8Array,
|
data: Uint8Array | ReadableStream<Uint8Array>,
|
||||||
options?: WriteFileOptions
|
options?: WriteFileOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (path instanceof URL && path.protocol !== 'file:') {
|
if (path instanceof URL && path.protocol !== 'file:') {
|
||||||
throw new TypeError('Must be a file URL.')
|
throw new TypeError('Must be a file URL.')
|
||||||
}
|
}
|
||||||
|
|
||||||
await invoke('plugin:fs|write_file', data, {
|
if (data instanceof ReadableStream) {
|
||||||
headers: {
|
const file = await open(path, options)
|
||||||
path: encodeURIComponent(path instanceof URL ? path.toString() : path),
|
for await (const chunk of data) {
|
||||||
options: JSON.stringify(options)
|
await file.write(chunk)
|
||||||
}
|
}
|
||||||
})
|
await file.close()
|
||||||
|
} else {
|
||||||
|
await invoke('plugin:fs|write_file', data, {
|
||||||
|
headers: {
|
||||||
|
path: encodeURIComponent(path instanceof URL ? path.toString() : path),
|
||||||
|
options: JSON.stringify(options)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1281,6 +1322,31 @@ async function watchImmediate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of a file or directory. For files, the `stat` functions can be used as well.
|
||||||
|
*
|
||||||
|
* If `path` is a directory, this function will recursively iterate over every file and every directory inside of `path` and therefore will be very time consuming if used on larger directories.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { size, BaseDirectory } from '@tauri-apps/plugin-fs';
|
||||||
|
* // Get the size of the `$APPDATA/tauri` directory.
|
||||||
|
* const dirSize = await size('tauri', { baseDir: BaseDirectory.AppData });
|
||||||
|
* console.log(dirSize); // 1024
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @since 2.1.0
|
||||||
|
*/
|
||||||
|
async function size(path: string | URL): Promise<number> {
|
||||||
|
if (path instanceof URL && path.protocol !== 'file:') {
|
||||||
|
throw new TypeError('Must be a file URL.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return await invoke('plugin:fs|size', {
|
||||||
|
path: path instanceof URL ? path.toString() : path
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
CreateOptions,
|
CreateOptions,
|
||||||
OpenOptions,
|
OpenOptions,
|
||||||
@@ -1328,5 +1394,6 @@ export {
|
|||||||
writeTextFile,
|
writeTextFile,
|
||||||
exists,
|
exists,
|
||||||
watch,
|
watch,
|
||||||
watchImmediate
|
watchImmediate,
|
||||||
|
size
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,18 @@
|
|||||||
[[permission]]
|
[[permission]]
|
||||||
identifier = "allow-read-text-file-lines"
|
identifier = "allow-read-text-file-lines"
|
||||||
description = "Enables the read_text_file_lines command without any pre-configured scope."
|
description = "Enables the read_text_file_lines command without any pre-configured scope."
|
||||||
commands.allow = ["read_text_file_lines"]
|
|
||||||
|
[permission.commands]
|
||||||
|
allow = [
|
||||||
|
"read_text_file_lines",
|
||||||
|
"read_text_file_lines_next",
|
||||||
|
]
|
||||||
|
deny = []
|
||||||
|
|
||||||
[[permission]]
|
[[permission]]
|
||||||
identifier = "deny-read-text-file-lines"
|
identifier = "deny-read-text-file-lines"
|
||||||
description = "Denies the read_text_file_lines command without any pre-configured scope."
|
description = "Denies the read_text_file_lines command without any pre-configured scope."
|
||||||
commands.deny = ["read_text_file_lines"]
|
|
||||||
|
[permission.commands]
|
||||||
|
allow = []
|
||||||
|
deny = ["read_text_file_lines"]
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-size"
|
||||||
|
description = "Enables the size command without any pre-configured scope."
|
||||||
|
commands.allow = ["size"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-size"
|
||||||
|
description = "Denies the size command without any pre-configured scope."
|
||||||
|
commands.deny = ["size"]
|
||||||
@@ -5,9 +5,19 @@
|
|||||||
[[permission]]
|
[[permission]]
|
||||||
identifier = "allow-write-file"
|
identifier = "allow-write-file"
|
||||||
description = "Enables the write_file command without any pre-configured scope."
|
description = "Enables the write_file command without any pre-configured scope."
|
||||||
commands.allow = ["write_file"]
|
|
||||||
|
[permission.commands]
|
||||||
|
allow = [
|
||||||
|
"write_file",
|
||||||
|
"open",
|
||||||
|
"write",
|
||||||
|
]
|
||||||
|
deny = []
|
||||||
|
|
||||||
[[permission]]
|
[[permission]]
|
||||||
identifier = "deny-write-file"
|
identifier = "deny-write-file"
|
||||||
description = "Denies the write_file command without any pre-configured scope."
|
description = "Denies the write_file command without any pre-configured scope."
|
||||||
commands.deny = ["write_file"]
|
|
||||||
|
[permission.commands]
|
||||||
|
allow = []
|
||||||
|
deny = ["write_file"]
|
||||||
|
|||||||
@@ -3410,6 +3410,32 @@ Denies the seek command without any pre-configured scope.
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
|
`fs:allow-size`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Enables the size command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`fs:deny-size`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Denies the size command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
`fs:allow-stat`
|
`fs:allow-stat`
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -3,4 +3,4 @@
|
|||||||
[[permission]]
|
[[permission]]
|
||||||
identifier = "read-meta"
|
identifier = "read-meta"
|
||||||
description = "This enables all index or metadata related commands without any pre-configured accessible paths."
|
description = "This enables all index or metadata related commands without any pre-configured accessible paths."
|
||||||
commands.allow = ["read_dir", "stat", "lstat", "fstat", "exists"]
|
commands.allow = ["read_dir", "stat", "lstat", "fstat", "exists", "size"]
|
||||||
|
|||||||
@@ -1589,6 +1589,16 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "deny-seek"
|
"const": "deny-seek"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the size command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "allow-size"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the size command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "deny-size"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the stat command without any pre-configured scope.",
|
"description": "Enables the stat command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
+225
-106
@@ -15,14 +15,14 @@ use tauri::{
|
|||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{BufReader, Lines, Read, Write},
|
io::{BufRead, BufReader, Read, Write},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
sync::Mutex,
|
sync::Mutex,
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{scope::Entry, Error, FsExt, SafeFilePath};
|
use crate::{scope::Entry, Error, SafeFilePath};
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum CommandError {
|
pub enum CommandError {
|
||||||
@@ -245,32 +245,12 @@ pub fn mkdir<R: Runtime>(
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct DirEntry {
|
pub struct DirEntry {
|
||||||
pub name: Option<String>,
|
pub name: String,
|
||||||
pub is_directory: bool,
|
pub is_directory: bool,
|
||||||
pub is_file: bool,
|
pub is_file: bool,
|
||||||
pub is_symlink: bool,
|
pub is_symlink: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_dir_inner<P: AsRef<Path>>(path: P) -> crate::Result<Vec<DirEntry>> {
|
|
||||||
let mut files_and_dirs: Vec<DirEntry> = vec![];
|
|
||||||
for entry in std::fs::read_dir(path)? {
|
|
||||||
let path = entry?.path();
|
|
||||||
let file_type = path.metadata()?.file_type();
|
|
||||||
files_and_dirs.push(DirEntry {
|
|
||||||
is_directory: file_type.is_dir(),
|
|
||||||
is_file: file_type.is_file(),
|
|
||||||
is_symlink: std::fs::symlink_metadata(&path)
|
|
||||||
.map(|md| md.file_type().is_symlink())
|
|
||||||
.unwrap_or(false),
|
|
||||||
name: path
|
|
||||||
.file_name()
|
|
||||||
.map(|name| name.to_string_lossy())
|
|
||||||
.map(|name| name.to_string()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Result::Ok(files_and_dirs)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn read_dir<R: Runtime>(
|
pub async fn read_dir<R: Runtime>(
|
||||||
webview: Webview<R>,
|
webview: Webview<R>,
|
||||||
@@ -287,14 +267,37 @@ pub async fn read_dir<R: Runtime>(
|
|||||||
options.as_ref().and_then(|o| o.base_dir),
|
options.as_ref().and_then(|o| o.base_dir),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
read_dir_inner(&resolved_path)
|
let entries = std::fs::read_dir(&resolved_path).map_err(|e| {
|
||||||
.map_err(|e| {
|
format!(
|
||||||
format!(
|
"failed to read directory at path: {} with error: {e}",
|
||||||
"failed to read directory at path: {} with error: {e}",
|
resolved_path.display()
|
||||||
resolved_path.display()
|
)
|
||||||
)
|
})?;
|
||||||
|
|
||||||
|
let entries = entries
|
||||||
|
.filter_map(|entry| {
|
||||||
|
let entry = entry.ok()?;
|
||||||
|
let name = entry.file_name().into_string().ok()?;
|
||||||
|
let metadata = entry.file_type();
|
||||||
|
macro_rules! method_or_false {
|
||||||
|
($method:ident) => {
|
||||||
|
if let Ok(metadata) = &metadata {
|
||||||
|
metadata.$method()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Some(DirEntry {
|
||||||
|
name,
|
||||||
|
is_file: method_or_false!(is_file),
|
||||||
|
is_directory: method_or_false!(is_dir),
|
||||||
|
is_symlink: method_or_false!(is_symlink),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.map_err(Into::into)
|
.collect();
|
||||||
|
|
||||||
|
Ok(entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -369,6 +372,7 @@ pub async fn read_file<R: Runtime>(
|
|||||||
Ok(tauri::ipc::Response::new(contents))
|
Ok(tauri::ipc::Response::new(contents))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO, remove in v3, rely on `read_file` command instead
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn read_text_file<R: Runtime>(
|
pub async fn read_text_file<R: Runtime>(
|
||||||
webview: Webview<R>,
|
webview: Webview<R>,
|
||||||
@@ -376,33 +380,8 @@ pub async fn read_text_file<R: Runtime>(
|
|||||||
command_scope: CommandScope<Entry>,
|
command_scope: CommandScope<Entry>,
|
||||||
path: SafeFilePath,
|
path: SafeFilePath,
|
||||||
options: Option<BaseOptions>,
|
options: Option<BaseOptions>,
|
||||||
) -> CommandResult<String> {
|
) -> CommandResult<tauri::ipc::Response> {
|
||||||
let (mut file, path) = resolve_file(
|
read_file(webview, global_scope, command_scope, path, options).await
|
||||||
&webview,
|
|
||||||
&global_scope,
|
|
||||||
&command_scope,
|
|
||||||
path,
|
|
||||||
OpenOptions {
|
|
||||||
base: BaseOptions {
|
|
||||||
base_dir: options.as_ref().and_then(|o| o.base_dir),
|
|
||||||
},
|
|
||||||
options: crate::OpenOptions {
|
|
||||||
read: true,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut contents = String::new();
|
|
||||||
|
|
||||||
file.read_to_string(&mut contents).map_err(|e| {
|
|
||||||
format!(
|
|
||||||
"failed to read file as text at path: {} with error: {e}",
|
|
||||||
path.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(contents)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -413,8 +392,6 @@ pub fn read_text_file_lines<R: Runtime>(
|
|||||||
path: SafeFilePath,
|
path: SafeFilePath,
|
||||||
options: Option<BaseOptions>,
|
options: Option<BaseOptions>,
|
||||||
) -> CommandResult<ResourceId> {
|
) -> CommandResult<ResourceId> {
|
||||||
use std::io::BufRead;
|
|
||||||
|
|
||||||
let resolved_path = resolve_path(
|
let resolved_path = resolve_path(
|
||||||
&webview,
|
&webview,
|
||||||
&global_scope,
|
&global_scope,
|
||||||
@@ -430,7 +407,7 @@ pub fn read_text_file_lines<R: Runtime>(
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let lines = BufReader::new(file).lines();
|
let lines = BufReader::new(file);
|
||||||
let rid = webview.resources_table().add(StdLinesResource::new(lines));
|
let rid = webview.resources_table().add(StdLinesResource::new(lines));
|
||||||
|
|
||||||
Ok(rid)
|
Ok(rid)
|
||||||
@@ -440,18 +417,28 @@ pub fn read_text_file_lines<R: Runtime>(
|
|||||||
pub async fn read_text_file_lines_next<R: Runtime>(
|
pub async fn read_text_file_lines_next<R: Runtime>(
|
||||||
webview: Webview<R>,
|
webview: Webview<R>,
|
||||||
rid: ResourceId,
|
rid: ResourceId,
|
||||||
) -> CommandResult<(Option<String>, bool)> {
|
) -> CommandResult<tauri::ipc::Response> {
|
||||||
let mut resource_table = webview.resources_table();
|
let mut resource_table = webview.resources_table();
|
||||||
let lines = resource_table.get::<StdLinesResource>(rid)?;
|
let lines = resource_table.get::<StdLinesResource>(rid)?;
|
||||||
|
|
||||||
let ret = StdLinesResource::with_lock(&lines, |lines| {
|
let ret = StdLinesResource::with_lock(&lines, |lines| -> CommandResult<Vec<u8>> {
|
||||||
lines.next().map(|a| (a.ok(), false)).unwrap_or_else(|| {
|
// This is an optimization to include wether we finished iteration or not (1 or 0)
|
||||||
let _ = resource_table.close(rid);
|
// at the end of returned vector so we can use `tauri::ipc::Response`
|
||||||
(None, true)
|
// and avoid serialization overhead of separate values.
|
||||||
})
|
match lines.next() {
|
||||||
|
Some(Ok(mut bytes)) => {
|
||||||
|
bytes.push(false as u8);
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
Some(Err(_)) => Ok(vec![false as u8]),
|
||||||
|
None => {
|
||||||
|
resource_table.close(rid)?;
|
||||||
|
Ok(vec![true as u8])
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(ret)
|
ret.map(tauri::ipc::Response::new)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
@@ -802,10 +789,11 @@ fn default_create_value() -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_file_inner<R: Runtime>(
|
#[tauri::command]
|
||||||
|
pub async fn write_file<R: Runtime>(
|
||||||
webview: Webview<R>,
|
webview: Webview<R>,
|
||||||
global_scope: &GlobalScope<Entry>,
|
global_scope: GlobalScope<Entry>,
|
||||||
command_scope: &CommandScope<Entry>,
|
command_scope: CommandScope<Entry>,
|
||||||
request: tauri::ipc::Request<'_>,
|
request: tauri::ipc::Request<'_>,
|
||||||
) -> CommandResult<()> {
|
) -> CommandResult<()> {
|
||||||
let data = match request.body() {
|
let data = match request.body() {
|
||||||
@@ -836,8 +824,8 @@ fn write_file_inner<R: Runtime>(
|
|||||||
|
|
||||||
let (mut file, path) = resolve_file(
|
let (mut file, path) = resolve_file(
|
||||||
&webview,
|
&webview,
|
||||||
global_scope,
|
&global_scope,
|
||||||
command_scope,
|
&command_scope,
|
||||||
path,
|
path,
|
||||||
if let Some(opts) = options {
|
if let Some(opts) = options {
|
||||||
OpenOptions {
|
OpenOptions {
|
||||||
@@ -880,17 +868,7 @@ fn write_file_inner<R: Runtime>(
|
|||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
// TODO, remove in v3, rely on `write_file` command instead
|
||||||
pub async fn write_file<R: Runtime>(
|
|
||||||
webview: Webview<R>,
|
|
||||||
global_scope: GlobalScope<Entry>,
|
|
||||||
command_scope: CommandScope<Entry>,
|
|
||||||
request: tauri::ipc::Request<'_>,
|
|
||||||
) -> CommandResult<()> {
|
|
||||||
write_file_inner(webview, &global_scope, &command_scope, request)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO, in v3, remove this command and rely on `write_file` command only
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn write_text_file<R: Runtime>(
|
pub async fn write_text_file<R: Runtime>(
|
||||||
webview: Webview<R>,
|
webview: Webview<R>,
|
||||||
@@ -898,7 +876,7 @@ pub async fn write_text_file<R: Runtime>(
|
|||||||
command_scope: CommandScope<Entry>,
|
command_scope: CommandScope<Entry>,
|
||||||
request: tauri::ipc::Request<'_>,
|
request: tauri::ipc::Request<'_>,
|
||||||
) -> CommandResult<()> {
|
) -> CommandResult<()> {
|
||||||
write_file_inner(webview, &global_scope, &command_scope, request)
|
write_file(webview, global_scope, command_scope, request).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -919,6 +897,55 @@ pub fn exists<R: Runtime>(
|
|||||||
Ok(resolved_path.exists())
|
Ok(resolved_path.exists())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn size<R: Runtime>(
|
||||||
|
webview: Webview<R>,
|
||||||
|
global_scope: GlobalScope<Entry>,
|
||||||
|
command_scope: CommandScope<Entry>,
|
||||||
|
path: SafeFilePath,
|
||||||
|
options: Option<BaseOptions>,
|
||||||
|
) -> CommandResult<u64> {
|
||||||
|
let resolved_path = resolve_path(
|
||||||
|
&webview,
|
||||||
|
&global_scope,
|
||||||
|
&command_scope,
|
||||||
|
path,
|
||||||
|
options.as_ref().and_then(|o| o.base_dir),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let metadata = resolved_path.metadata()?;
|
||||||
|
|
||||||
|
if metadata.is_file() {
|
||||||
|
Ok(metadata.len())
|
||||||
|
} else {
|
||||||
|
let size = get_dir_size(&resolved_path).map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"failed to get size at path: {} with error: {e}",
|
||||||
|
resolved_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_dir_size(path: &PathBuf) -> CommandResult<u64> {
|
||||||
|
let mut size = 0;
|
||||||
|
|
||||||
|
for entry in std::fs::read_dir(path)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let metadata = entry.metadata()?;
|
||||||
|
|
||||||
|
if metadata.is_file() {
|
||||||
|
size += metadata.len();
|
||||||
|
} else if metadata.is_dir() {
|
||||||
|
size += get_dir_size(&entry.path())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(size)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
pub fn resolve_file<R: Runtime>(
|
pub fn resolve_file<R: Runtime>(
|
||||||
webview: &Webview<R>,
|
webview: &Webview<R>,
|
||||||
@@ -964,6 +991,8 @@ pub fn resolve_file<R: Runtime>(
|
|||||||
path: SafeFilePath,
|
path: SafeFilePath,
|
||||||
open_options: OpenOptions,
|
open_options: OpenOptions,
|
||||||
) -> CommandResult<(File, PathBuf)> {
|
) -> CommandResult<(File, PathBuf)> {
|
||||||
|
use crate::FsExt;
|
||||||
|
|
||||||
match path {
|
match path {
|
||||||
SafeFilePath::Url(url) => {
|
SafeFilePath::Url(url) => {
|
||||||
let path = url.as_str().into();
|
let path = url.as_str().into();
|
||||||
@@ -996,40 +1025,81 @@ pub fn resolve_path<R: Runtime>(
|
|||||||
path
|
path
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let fs_scope = webview.state::<crate::Scope>();
|
||||||
|
|
||||||
let scope = tauri::scope::fs::Scope::new(
|
let scope = tauri::scope::fs::Scope::new(
|
||||||
webview,
|
webview,
|
||||||
&FsScope::Scope {
|
&FsScope::Scope {
|
||||||
allow: webview
|
allow: global_scope
|
||||||
.fs_scope()
|
.allows()
|
||||||
.allowed
|
.iter()
|
||||||
.lock()
|
.filter_map(|e| e.path.clone())
|
||||||
.unwrap()
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.chain(global_scope.allows().iter().filter_map(|e| e.path.clone()))
|
|
||||||
.chain(command_scope.allows().iter().filter_map(|e| e.path.clone()))
|
.chain(command_scope.allows().iter().filter_map(|e| e.path.clone()))
|
||||||
.collect(),
|
.collect(),
|
||||||
deny: webview
|
deny: global_scope
|
||||||
.fs_scope()
|
.denies()
|
||||||
.denied
|
.iter()
|
||||||
.lock()
|
.filter_map(|e| e.path.clone())
|
||||||
.unwrap()
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.chain(global_scope.denies().iter().filter_map(|e| e.path.clone()))
|
|
||||||
.chain(command_scope.denies().iter().filter_map(|e| e.path.clone()))
|
.chain(command_scope.denies().iter().filter_map(|e| e.path.clone()))
|
||||||
.collect(),
|
.collect(),
|
||||||
require_literal_leading_dot: webview.fs_scope().require_literal_leading_dot,
|
require_literal_leading_dot: fs_scope.require_literal_leading_dot,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if scope.is_allowed(&path) {
|
let require_literal_leading_dot = fs_scope.require_literal_leading_dot.unwrap_or(cfg!(unix));
|
||||||
|
|
||||||
|
if is_forbidden(&fs_scope.scope, &path, require_literal_leading_dot)
|
||||||
|
|| is_forbidden(&scope, &path, require_literal_leading_dot)
|
||||||
|
{
|
||||||
|
return Err(CommandError::Plugin(Error::PathForbidden(path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if fs_scope.scope.is_allowed(&path) || scope.is_allowed(&path) {
|
||||||
Ok(path)
|
Ok(path)
|
||||||
} else {
|
} else {
|
||||||
Err(CommandError::Plugin(Error::PathForbidden(path)))
|
Err(CommandError::Plugin(Error::PathForbidden(path)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_forbidden<P: AsRef<Path>>(
|
||||||
|
scope: &tauri::fs::Scope,
|
||||||
|
path: P,
|
||||||
|
require_literal_leading_dot: bool,
|
||||||
|
) -> bool {
|
||||||
|
let path = path.as_ref();
|
||||||
|
let path = if path.is_symlink() {
|
||||||
|
match std::fs::read_link(path) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => return false,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path.to_path_buf()
|
||||||
|
};
|
||||||
|
let path = if !path.exists() {
|
||||||
|
crate::Result::Ok(path)
|
||||||
|
} else {
|
||||||
|
std::fs::canonicalize(path).map_err(Into::into)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(path) = path {
|
||||||
|
let path: PathBuf = path.components().collect();
|
||||||
|
scope.forbidden_patterns().iter().any(|p| {
|
||||||
|
p.matches_path_with(
|
||||||
|
&path,
|
||||||
|
glob::MatchOptions {
|
||||||
|
// this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt`
|
||||||
|
// see: <https://github.com/tauri-apps/tauri/security/advisories/GHSA-6mv3-wm7j-h4w5>
|
||||||
|
require_literal_separator: true,
|
||||||
|
require_literal_leading_dot,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct StdFileResource(Mutex<File>);
|
struct StdFileResource(Mutex<File>);
|
||||||
|
|
||||||
impl StdFileResource {
|
impl StdFileResource {
|
||||||
@@ -1045,14 +1115,38 @@ impl StdFileResource {
|
|||||||
|
|
||||||
impl Resource for StdFileResource {}
|
impl Resource for StdFileResource {}
|
||||||
|
|
||||||
struct StdLinesResource(Mutex<Lines<BufReader<File>>>);
|
/// Same as [std::io::Lines] but with bytes
|
||||||
|
struct LinesBytes<T: BufRead>(T);
|
||||||
|
|
||||||
|
impl<B: BufRead> Iterator for LinesBytes<B> {
|
||||||
|
type Item = std::io::Result<Vec<u8>>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<std::io::Result<Vec<u8>>> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
match self.0.read_until(b'\n', &mut buf) {
|
||||||
|
Ok(0) => None,
|
||||||
|
Ok(_n) => {
|
||||||
|
if buf.last() == Some(&b'\n') {
|
||||||
|
buf.pop();
|
||||||
|
if buf.last() == Some(&b'\r') {
|
||||||
|
buf.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Ok(buf))
|
||||||
|
}
|
||||||
|
Err(e) => Some(Err(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StdLinesResource(Mutex<LinesBytes<BufReader<File>>>);
|
||||||
|
|
||||||
impl StdLinesResource {
|
impl StdLinesResource {
|
||||||
fn new(lines: Lines<BufReader<File>>) -> Self {
|
fn new(lines: BufReader<File>) -> Self {
|
||||||
Self(Mutex::new(lines))
|
Self(Mutex::new(LinesBytes(lines)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_lock<R, F: FnMut(&mut Lines<BufReader<File>>) -> R>(&self, mut f: F) -> R {
|
fn with_lock<R, F: FnMut(&mut LinesBytes<BufReader<File>>) -> R>(&self, mut f: F) -> R {
|
||||||
let mut lines = self.0.lock().unwrap();
|
let mut lines = self.0.lock().unwrap();
|
||||||
f(&mut lines)
|
f(&mut lines)
|
||||||
}
|
}
|
||||||
@@ -1151,7 +1245,12 @@ fn get_stat(metadata: std::fs::Metadata) -> FileInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use std::io::{BufRead, BufReader};
|
||||||
|
|
||||||
|
use super::LinesBytes;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn safe_file_path_parse() {
|
fn safe_file_path_parse() {
|
||||||
use super::SafeFilePath;
|
use super::SafeFilePath;
|
||||||
@@ -1165,4 +1264,24 @@ mod test {
|
|||||||
Ok(SafeFilePath::Url(_))
|
Ok(SafeFilePath::Url(_))
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lines_bytes() {
|
||||||
|
let base = String::from("line 1\nline2\nline 3\nline 4");
|
||||||
|
let bytes = base.as_bytes();
|
||||||
|
|
||||||
|
let string1 = base.lines().collect::<String>();
|
||||||
|
let string2 = BufReader::new(bytes)
|
||||||
|
.lines()
|
||||||
|
.map_while(Result::ok)
|
||||||
|
.collect::<String>();
|
||||||
|
let string3 = LinesBytes(BufReader::new(bytes))
|
||||||
|
.flatten()
|
||||||
|
.flat_map(String::from_utf8)
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
assert_eq!(string1, string2);
|
||||||
|
assert_eq!(string1, string3);
|
||||||
|
assert_eq!(string2, string3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-17
@@ -2,8 +2,6 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//! [](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/fs)
|
|
||||||
//!
|
|
||||||
//! Access the file system.
|
//! Access the file system.
|
||||||
|
|
||||||
#![doc(
|
#![doc(
|
||||||
@@ -17,7 +15,7 @@ use serde::Deserialize;
|
|||||||
use tauri::{
|
use tauri::{
|
||||||
ipc::ScopeObject,
|
ipc::ScopeObject,
|
||||||
plugin::{Builder as PluginBuilder, TauriPlugin},
|
plugin::{Builder as PluginBuilder, TauriPlugin},
|
||||||
utils::acl::Value,
|
utils::{acl::Value, config::FsScope},
|
||||||
AppHandle, DragDropEvent, Manager, RunEvent, Runtime, WindowEvent,
|
AppHandle, DragDropEvent, Manager, RunEvent, Runtime, WindowEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41,7 +39,6 @@ pub use desktop::Fs;
|
|||||||
pub use mobile::Fs;
|
pub use mobile::Fs;
|
||||||
|
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
pub use scope::{Event as ScopeEvent, Scope};
|
|
||||||
|
|
||||||
pub use file_path::FilePath;
|
pub use file_path::FilePath;
|
||||||
pub use file_path::SafeFilePath;
|
pub use file_path::SafeFilePath;
|
||||||
@@ -367,21 +364,26 @@ impl ScopeObject for scope::Entry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Scope {
|
||||||
|
pub(crate) scope: tauri::fs::Scope,
|
||||||
|
pub(crate) require_literal_leading_dot: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
pub trait FsExt<R: Runtime> {
|
pub trait FsExt<R: Runtime> {
|
||||||
fn fs_scope(&self) -> &Scope;
|
fn fs_scope(&self) -> tauri::fs::Scope;
|
||||||
fn try_fs_scope(&self) -> Option<&Scope>;
|
fn try_fs_scope(&self) -> Option<tauri::fs::Scope>;
|
||||||
|
|
||||||
/// Cross platform file system APIs that also support manipulating Android files.
|
/// Cross platform file system APIs that also support manipulating Android files.
|
||||||
fn fs(&self) -> &Fs<R>;
|
fn fs(&self) -> &Fs<R>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Runtime, T: Manager<R>> FsExt<R> for T {
|
impl<R: Runtime, T: Manager<R>> FsExt<R> for T {
|
||||||
fn fs_scope(&self) -> &Scope {
|
fn fs_scope(&self) -> tauri::fs::Scope {
|
||||||
self.state::<Scope>().inner()
|
self.state::<Scope>().scope.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_fs_scope(&self) -> Option<&Scope> {
|
fn try_fs_scope(&self) -> Option<tauri::fs::Scope> {
|
||||||
self.try_state::<Scope>().map(|s| s.inner())
|
self.try_state::<Scope>().map(|s| s.scope.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fs(&self) -> &Fs<R> {
|
fn fs(&self) -> &Fs<R> {
|
||||||
@@ -415,17 +417,20 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
|
|||||||
commands::write_file,
|
commands::write_file,
|
||||||
commands::write_text_file,
|
commands::write_text_file,
|
||||||
commands::exists,
|
commands::exists,
|
||||||
|
commands::size,
|
||||||
#[cfg(feature = "watch")]
|
#[cfg(feature = "watch")]
|
||||||
watcher::watch,
|
watcher::watch,
|
||||||
#[cfg(feature = "watch")]
|
#[cfg(feature = "watch")]
|
||||||
watcher::unwatch
|
watcher::unwatch
|
||||||
])
|
])
|
||||||
.setup(|app, api| {
|
.setup(|app, api| {
|
||||||
let mut scope = Scope::default();
|
let scope = Scope {
|
||||||
scope.require_literal_leading_dot = api
|
require_literal_leading_dot: api
|
||||||
.config()
|
.config()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|c| c.require_literal_leading_dot);
|
.and_then(|c| c.require_literal_leading_dot),
|
||||||
|
scope: tauri::fs::Scope::new(app, &FsScope::default())?,
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
{
|
{
|
||||||
@@ -448,9 +453,9 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
|
|||||||
let scope = app.fs_scope();
|
let scope = app.fs_scope();
|
||||||
for path in paths {
|
for path in paths {
|
||||||
if path.is_file() {
|
if path.is_file() {
|
||||||
scope.allow_file(path);
|
let _ = scope.allow_file(path);
|
||||||
} else {
|
} else {
|
||||||
scope.allow_directory(path, true);
|
let _ = scope.allow_directory(path, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-118
@@ -2,130 +2,18 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
use std::{
|
use std::path::PathBuf;
|
||||||
collections::HashMap,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::{
|
|
||||||
atomic::{AtomicU32, Ordering},
|
|
||||||
Mutex,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Entry {
|
||||||
|
pub path: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub(crate) enum EntryRaw {
|
pub(crate) enum EntryRaw {
|
||||||
Value(PathBuf),
|
Value(PathBuf),
|
||||||
Object { path: PathBuf },
|
Object { path: PathBuf },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Entry {
|
|
||||||
pub path: Option<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type EventId = u32;
|
|
||||||
type EventListener = Box<dyn Fn(&Event) + Send>;
|
|
||||||
|
|
||||||
/// Scope change event.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum Event {
|
|
||||||
/// A path has been allowed.
|
|
||||||
PathAllowed(PathBuf),
|
|
||||||
/// A path has been forbidden.
|
|
||||||
PathForbidden(PathBuf),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Scope {
|
|
||||||
pub(crate) allowed: Mutex<Vec<PathBuf>>,
|
|
||||||
pub(crate) denied: Mutex<Vec<PathBuf>>,
|
|
||||||
event_listeners: Mutex<HashMap<EventId, EventListener>>,
|
|
||||||
next_event_id: AtomicU32,
|
|
||||||
pub(crate) require_literal_leading_dot: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Scope {
|
|
||||||
/// Extend the allowed patterns with the given directory.
|
|
||||||
///
|
|
||||||
/// After this function has been called, the frontend will be able to use the Tauri API to read
|
|
||||||
/// the directory and all of its files. If `recursive` is `true`, subdirectories will be accessible too.
|
|
||||||
pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) {
|
|
||||||
let path = path.as_ref();
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut allowed = self.allowed.lock().unwrap();
|
|
||||||
allowed.push(path.to_path_buf());
|
|
||||||
allowed.push(path.join(if recursive { "**" } else { "*" }));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.emit(Event::PathAllowed(path.to_path_buf()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extend the allowed patterns with the given file path.
|
|
||||||
///
|
|
||||||
/// After this function has been called, the frontend will be able to use the Tauri API to read the contents of this file.
|
|
||||||
pub fn allow_file<P: AsRef<Path>>(&self, path: P) {
|
|
||||||
let path = path.as_ref();
|
|
||||||
|
|
||||||
self.allowed.lock().unwrap().push(path.to_path_buf());
|
|
||||||
|
|
||||||
self.emit(Event::PathAllowed(path.to_path_buf()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the given directory path to be forbidden by this scope.
|
|
||||||
///
|
|
||||||
/// **Note:** this takes precedence over allowed paths, so its access gets denied **always**.
|
|
||||||
pub fn forbid_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) {
|
|
||||||
let path = path.as_ref();
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut denied = self.denied.lock().unwrap();
|
|
||||||
denied.push(path.to_path_buf());
|
|
||||||
denied.push(path.join(if recursive { "**" } else { "*" }));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.emit(Event::PathForbidden(path.to_path_buf()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the given file path to be forbidden by this scope.
|
|
||||||
///
|
|
||||||
/// **Note:** this takes precedence over allowed paths, so its access gets denied **always**.
|
|
||||||
pub fn forbid_file<P: AsRef<Path>>(&self, path: P) {
|
|
||||||
let path = path.as_ref();
|
|
||||||
|
|
||||||
self.denied.lock().unwrap().push(path.to_path_buf());
|
|
||||||
|
|
||||||
self.emit(Event::PathForbidden(path.to_path_buf()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List of allowed paths.
|
|
||||||
pub fn allowed(&self) -> Vec<PathBuf> {
|
|
||||||
self.allowed.lock().unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List of forbidden paths.
|
|
||||||
pub fn forbidden(&self) -> Vec<PathBuf> {
|
|
||||||
self.denied.lock().unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_event_id(&self) -> u32 {
|
|
||||||
self.next_event_id.fetch_add(1, Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit(&self, event: Event) {
|
|
||||||
let listeners = self.event_listeners.lock().unwrap();
|
|
||||||
let handlers = listeners.values();
|
|
||||||
for listener in handlers {
|
|
||||||
listener(&event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Listen to an event on this scope.
|
|
||||||
pub fn listen<F: Fn(&Event) + Send + 'static>(&self, f: F) -> EventId {
|
|
||||||
let id = self.next_event_id();
|
|
||||||
self.event_listeners.lock().unwrap().insert(id, Box::new(f));
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -92,6 +92,20 @@ fn main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Then, for instance, grant the plugin the permission to check or request permissions from the user and to read the device position
|
||||||
|
|
||||||
|
`src-tauri/capabilities/default.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"geolocation:allow-check-permissions",
|
||||||
|
"geolocation:allow-request-permissions",
|
||||||
|
"geolocation:allow-get-current-position",
|
||||||
|
"geolocation:allow-watch-position",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
Afterwards all the plugin's APIs are available through the JavaScript guest bindings:
|
Afterwards all the plugin's APIs are available through the JavaScript guest bindings:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
@@ -100,7 +114,7 @@ import {
|
|||||||
requestPermissions,
|
requestPermissions,
|
||||||
getCurrentPosition,
|
getCurrentPosition,
|
||||||
watchPosition
|
watchPosition
|
||||||
} from '@tauri-apps/plugin-log'
|
} from '@tauri-apps/plugin-geolocation'
|
||||||
|
|
||||||
let permissions = await checkPermissions()
|
let permissions = await checkPermissions()
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
if("__TAURI__"in window){var __TAURI_PLUGIN_GEOLOCATION__=function(t){"use strict";function e(t,e,n,i){if("a"===n&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof e?t!==e||!i:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===n?i:"a"===n?i.call(t):i?i.value:e.get(t)}function n(t,e,n,i,o){if("function"==typeof e?t!==e||!o:!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(t,n),n}var i,o,s;"function"==typeof SuppressedError&&SuppressedError;class r{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,i.set(this,(()=>{})),o.set(this,0),s.set(this,{}),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}((({message:t,id:r})=>{if(r===e(this,o,"f")){n(this,o,r+1),e(this,i,"f").call(this,t);const a=Object.keys(e(this,s,"f"));if(a.length>0){let t=r+1;for(const n of a.sort()){if(parseInt(n)!==t)break;{const o=e(this,s,"f")[n];delete e(this,s,"f")[n],e(this,i,"f").call(this,o),t+=1}}n(this,o,t)}}else e(this,s,"f")[r.toString()]=t}))}set onmessage(t){n(this,i,t)}get onmessage(){return e(this,i,"f")}toJSON(){return`__CHANNEL__:${this.id}`}}async function a(t,e={},n){return window.__TAURI_INTERNALS__.invoke(t,e,n)}return i=new WeakMap,o=new WeakMap,s=new WeakMap,t.checkPermissions=async function(){return await async function(t){return a(`plugin:${t}|check_permissions`)}("geolocation")},t.clearWatch=async function(t){await a("plugin:geolocation|clear_watch",{channelId:t})},t.getCurrentPosition=async function(t){return await a("plugin:geolocation|get_current_position",{options:t})},t.requestPermissions=async function(t){return await a("plugin:geolocation|request_permissions",{permissions:t})},t.watchPosition=async function(t,e){const n=new r;return n.onmessage=t=>{"string"==typeof t?e(null,t):e(t)},await a("plugin:geolocation|watch_position",{options:t,channel:n}),n.id},t}({});Object.defineProperty(window.__TAURI__,"geolocation",{value:__TAURI_PLUGIN_GEOLOCATION__})}
|
if("__TAURI__"in window){var __TAURI_PLUGIN_GEOLOCATION__=function(t){"use strict";function e(t,e,n,i){if("a"===n&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof e?t!==e||!i:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===n?i:"a"===n?i.call(t):i?i.value:e.get(t)}function n(t,e,n,i,o){if("function"==typeof e?t!==e||!o:!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(t,n),n}var i,o,s;"function"==typeof SuppressedError&&SuppressedError;const r="__TAURI_TO_IPC_KEY__";class a{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,i.set(this,(()=>{})),o.set(this,0),s.set(this,{}),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}((({message:t,id:r})=>{if(r===e(this,o,"f")){n(this,o,r+1),e(this,i,"f").call(this,t);const a=Object.keys(e(this,s,"f"));if(a.length>0){let t=r+1;for(const n of a.sort()){if(parseInt(n)!==t)break;{const o=e(this,s,"f")[n];delete e(this,s,"f")[n],e(this,i,"f").call(this,o),t+=1}}n(this,o,t)}}else e(this,s,"f")[r.toString()]=t}))}set onmessage(t){n(this,i,t)}get onmessage(){return e(this,i,"f")}[(i=new WeakMap,o=new WeakMap,s=new WeakMap,r)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[r]()}}async function c(t,e={},n){return window.__TAURI_INTERNALS__.invoke(t,e,n)}return t.checkPermissions=async function(){return await async function(t){return c(`plugin:${t}|check_permissions`)}("geolocation")},t.clearWatch=async function(t){await c("plugin:geolocation|clear_watch",{channelId:t})},t.getCurrentPosition=async function(t){return await c("plugin:geolocation|get_current_position",{options:t})},t.requestPermissions=async function(t){return await c("plugin:geolocation|request_permissions",{permissions:t})},t.watchPosition=async function(t,e){const n=new a;return n.onmessage=t=>{"string"==typeof t?e(null,t):e(t)},await c("plugin:geolocation|watch_position",{options:t,channel:n}),n.id},t}({});Object.defineProperty(window.__TAURI__,"geolocation",{value:__TAURI_PLUGIN_GEOLOCATION__})}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
if("__TAURI__"in window){var __TAURI_PLUGIN_GLOBAL_SHORTCUT__=function(t){"use strict";function e(t,e,r,s){if("a"===r&&!s)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof e?t!==e||!s:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?s:"a"===r?s.call(t):s?s.value:e.get(t)}function r(t,e,r,s,n){if("function"==typeof e?t!==e||!n:!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(t,r),r}var s,n,i;"function"==typeof SuppressedError&&SuppressedError;class o{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,s.set(this,(()=>{})),n.set(this,0),i.set(this,{}),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}((({message:t,id:o})=>{if(o===e(this,n,"f")){r(this,n,o+1),e(this,s,"f").call(this,t);const a=Object.keys(e(this,i,"f"));if(a.length>0){let t=o+1;for(const r of a.sort()){if(parseInt(r)!==t)break;{const n=e(this,i,"f")[r];delete e(this,i,"f")[r],e(this,s,"f").call(this,n),t+=1}}r(this,n,t)}}else e(this,i,"f")[o.toString()]=t}))}set onmessage(t){r(this,s,t)}get onmessage(){return e(this,s,"f")}toJSON(){return`__CHANNEL__:${this.id}`}}async function a(t,e={},r){return window.__TAURI_INTERNALS__.invoke(t,e,r)}return s=new WeakMap,n=new WeakMap,i=new WeakMap,t.isRegistered=async function(t){return await a("plugin:global-shortcut|is_registered",{shortcut:t})},t.register=async function(t,e){const r=new o;return r.onmessage=e,await a("plugin:global-shortcut|register",{shortcuts:Array.isArray(t)?t:[t],handler:r})},t.unregister=async function(t){return await a("plugin:global-shortcut|unregister",{shortcuts:Array.isArray(t)?t:[t]})},t.unregisterAll=async function(){return await a("plugin:global-shortcut|unregister_all",{})},t}({});Object.defineProperty(window.__TAURI__,"globalShortcut",{value:__TAURI_PLUGIN_GLOBAL_SHORTCUT__})}
|
if("__TAURI__"in window){var __TAURI_PLUGIN_GLOBAL_SHORTCUT__=function(t){"use strict";function e(t,e,r,s){if("a"===r&&!s)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof e?t!==e||!s:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?s:"a"===r?s.call(t):s?s.value:e.get(t)}function r(t,e,r,s,n){if("function"==typeof e?t!==e||!n:!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(t,r),r}var s,n,i;"function"==typeof SuppressedError&&SuppressedError;const o="__TAURI_TO_IPC_KEY__";class a{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,s.set(this,(()=>{})),n.set(this,0),i.set(this,{}),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}((({message:t,id:o})=>{if(o===e(this,n,"f")){r(this,n,o+1),e(this,s,"f").call(this,t);const a=Object.keys(e(this,i,"f"));if(a.length>0){let t=o+1;for(const r of a.sort()){if(parseInt(r)!==t)break;{const n=e(this,i,"f")[r];delete e(this,i,"f")[r],e(this,s,"f").call(this,n),t+=1}}r(this,n,t)}}else e(this,i,"f")[o.toString()]=t}))}set onmessage(t){r(this,s,t)}get onmessage(){return e(this,s,"f")}[(s=new WeakMap,n=new WeakMap,i=new WeakMap,o)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[o]()}}async function _(t,e={},r){return window.__TAURI_INTERNALS__.invoke(t,e,r)}return t.isRegistered=async function(t){return await _("plugin:global-shortcut|is_registered",{shortcut:t})},t.register=async function(t,e){const r=new a;return r.onmessage=e,await _("plugin:global-shortcut|register",{shortcuts:Array.isArray(t)?t:[t],handler:r})},t.unregister=async function(t){return await _("plugin:global-shortcut|unregister",{shortcuts:Array.isArray(t)?t:[t]})},t.unregisterAll=async function(){return await _("plugin:global-shortcut|unregister_all",{})},t}({});Object.defineProperty(window.__TAURI__,"globalShortcut",{value:__TAURI_PLUGIN_GLOBAL_SHORTCUT__})}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//! [](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/global-shortcut)
|
|
||||||
//!
|
|
||||||
//! Register global shortcuts.
|
//! Register global shortcuts.
|
||||||
//!
|
//!
|
||||||
//! - Supported platforms: Windows, Linux and macOS.
|
//! - Supported platforms: Windows, Linux and macOS.
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ http = "1"
|
|||||||
reqwest = { version = "0.12", default-features = false }
|
reqwest = { version = "0.12", default-features = false }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
data-url = "0.3"
|
data-url = "0.3"
|
||||||
|
tracing = { workspace = true, optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [
|
default = [
|
||||||
@@ -71,3 +72,4 @@ http2 = ["reqwest/http2"]
|
|||||||
charset = ["reqwest/charset"]
|
charset = ["reqwest/charset"]
|
||||||
macos-system-configuration = ["reqwest/macos-system-configuration"]
|
macos-system-configuration = ["reqwest/macos-system-configuration"]
|
||||||
unsafe-headers = []
|
unsafe-headers = []
|
||||||
|
tracing = ["dep:tracing"]
|
||||||
|
|||||||
@@ -283,6 +283,9 @@ pub async fn fetch<R: Runtime>(
|
|||||||
|
|
||||||
request = request.headers(headers);
|
request = request.headers(headers);
|
||||||
|
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::trace!("{:?}", request);
|
||||||
|
|
||||||
let fut = async move { request.send().await.map_err(Into::into) };
|
let fut = async move { request.send().await.map_err(Into::into) };
|
||||||
let mut resources_table = webview.resources_table();
|
let mut resources_table = webview.resources_table();
|
||||||
let rid = resources_table.add_request(Box::pin(fut));
|
let rid = resources_table.add_request(Box::pin(fut));
|
||||||
@@ -304,6 +307,9 @@ pub async fn fetch<R: Runtime>(
|
|||||||
.header(header::CONTENT_TYPE, data_url.mime_type().to_string())
|
.header(header::CONTENT_TYPE, data_url.mime_type().to_string())
|
||||||
.body(reqwest::Body::from(body))?;
|
.body(reqwest::Body::from(body))?;
|
||||||
|
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::trace!("{:?}", response);
|
||||||
|
|
||||||
let fut = async move { Ok(reqwest::Response::from(response)) };
|
let fut = async move { Ok(reqwest::Response::from(response)) };
|
||||||
let mut resources_table = webview.resources_table();
|
let mut resources_table = webview.resources_table();
|
||||||
let rid = resources_table.add_request(Box::pin(fut));
|
let rid = resources_table.add_request(Box::pin(fut));
|
||||||
@@ -351,6 +357,9 @@ pub async fn fetch_send<R: Runtime>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::trace!("{:?}", res);
|
||||||
|
|
||||||
let status = res.status();
|
let status = res.status();
|
||||||
let url = res.url().to_string();
|
let url = res.url().to_string();
|
||||||
let mut headers = Vec::new();
|
let mut headers = Vec::new();
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//! [](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/http)
|
|
||||||
//!
|
|
||||||
//! Access the HTTP client written in Rust.
|
//! Access the HTTP client written in Rust.
|
||||||
|
|
||||||
pub use reqwest;
|
pub use reqwest;
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//! [](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/localhost)
|
|
||||||
//!
|
|
||||||
//! Expose your apps assets through a localhost server instead of the default custom protocol.
|
//! Expose your apps assets through a localhost server instead of the default custom protocol.
|
||||||
//!
|
//!
|
||||||
//! **Note: This plugins brings considerable security risks and you should only use it if you know what your are doing. If in doubt, use the default custom protocol implementation.**
|
//! **Note: This plugins brings considerable security risks and you should only use it if you know what your are doing. If in doubt, use the default custom protocol implementation.**
|
||||||
@@ -46,6 +44,7 @@ type OnRequest = Option<Box<dyn Fn(&Request, &mut Response) + Send + Sync>>;
|
|||||||
|
|
||||||
pub struct Builder {
|
pub struct Builder {
|
||||||
port: u16,
|
port: u16,
|
||||||
|
host: Option<String>,
|
||||||
on_request: OnRequest,
|
on_request: OnRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,10 +52,17 @@ impl Builder {
|
|||||||
pub fn new(port: u16) -> Self {
|
pub fn new(port: u16) -> Self {
|
||||||
Self {
|
Self {
|
||||||
port,
|
port,
|
||||||
|
host: None,
|
||||||
on_request: None,
|
on_request: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Change the host the plugin binds to. Defaults to `localhost`.
|
||||||
|
pub fn host<H: Into<String>>(mut self, host: H) -> Self {
|
||||||
|
self.host = Some(host.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn on_request<F: Fn(&Request, &mut Response) + Send + Sync + 'static>(
|
pub fn on_request<F: Fn(&Request, &mut Response) + Send + Sync + 'static>(
|
||||||
mut self,
|
mut self,
|
||||||
f: F,
|
f: F,
|
||||||
@@ -67,6 +73,7 @@ impl Builder {
|
|||||||
|
|
||||||
pub fn build<R: Runtime>(mut self) -> TauriPlugin<R> {
|
pub fn build<R: Runtime>(mut self) -> TauriPlugin<R> {
|
||||||
let port = self.port;
|
let port = self.port;
|
||||||
|
let host = self.host.unwrap_or("localhost".to_string());
|
||||||
let on_request = self.on_request.take();
|
let on_request = self.on_request.take();
|
||||||
|
|
||||||
PluginBuilder::new("localhost")
|
PluginBuilder::new("localhost")
|
||||||
@@ -74,7 +81,7 @@ impl Builder {
|
|||||||
let asset_resolver = app.asset_resolver();
|
let asset_resolver = app.asset_resolver();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let server =
|
let server =
|
||||||
Server::http(format!("localhost:{port}")).expect("Unable to spawn server");
|
Server::http(format!("{host}:{port}")).expect("Unable to spawn server");
|
||||||
for req in server.incoming_requests() {
|
for req in server.incoming_requests() {
|
||||||
let path = req
|
let path = req
|
||||||
.url()
|
.url()
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ tauri-plugin = { workspace = true, features = ["build"] }
|
|||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tauri = { workspace = true }
|
tauri = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
serde_repr = "0.1"
|
serde_repr = "0.1"
|
||||||
byte-unit = "5"
|
byte-unit = "5"
|
||||||
log = { workspace = true, features = ["kv_unstable"] }
|
log = { workspace = true, features = ["kv_unstable"] }
|
||||||
time = { version = "0.3", features = ["formatting", "local-offset"] }
|
time = { version = "0.3", features = ["formatting", "local-offset"] }
|
||||||
fern = "0.7"
|
fern = "0.7"
|
||||||
thiserror = "1"
|
|
||||||
|
|
||||||
[target."cfg(target_os = \"android\")".dependencies]
|
[target."cfg(target_os = \"android\")".dependencies]
|
||||||
android_logger = "0.14"
|
android_logger = "0.14"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
if("__TAURI__"in window){var __TAURI_PLUGIN_LOG__=function(e){"use strict";function n(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}async function r(e,n={},r){return window.__TAURI_INTERNALS__.invoke(e,n,r)}var a,t;async function o(e,a,t){const o={kind:"Any"};return r("plugin:event|listen",{event:e,target:o,handler:n(a)}).then((n=>async()=>async function(e,n){await r("plugin:event|unlisten",{event:e,eventId:n})}(e,n)))}async function i(e,n,a){const t=(new Error).stack?.split("\n").map((e=>e.split("@"))),o=t?.filter((([e,n])=>e.length>0&&"[native code]"!==n)),{file:i,line:c,keyValues:u}=a??{};let l=o?.[0]?.filter((e=>e.length>0)).join("@");"Error"===l&&(l="webview::unknown"),await r("plugin:log|log",{level:e,message:n,location:l,file:i,line:c,keyValues:u})}async function c(e){return await o("log://log",(n=>{const{level:r}=n.payload;let{message:a}=n.payload;a=a.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,""),e({message:a,level:r})}))}return"function"==typeof SuppressedError&&SuppressedError,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG_ENTER="tauri://drag-enter",e.DRAG_OVER="tauri://drag-over",e.DRAG_DROP="tauri://drag-drop",e.DRAG_LEAVE="tauri://drag-leave"}(a||(a={})),function(e){e[e.Trace=1]="Trace",e[e.Debug=2]="Debug",e[e.Info=3]="Info",e[e.Warn=4]="Warn",e[e.Error=5]="Error"}(t||(t={})),e.attachConsole=async function(){return await c((({level:e,message:n})=>{switch(e){case t.Trace:console.log(n);break;case t.Debug:console.debug(n);break;case t.Info:console.info(n);break;case t.Warn:console.warn(n);break;case t.Error:console.error(n);break;default:throw new Error(`unknown log level ${e}`)}}))},e.attachLogger=c,e.debug=async function(e,n){await i(t.Debug,e,n)},e.error=async function(e,n){await i(t.Error,e,n)},e.info=async function(e,n){await i(t.Info,e,n)},e.trace=async function(e,n){await i(t.Trace,e,n)},e.warn=async function(e,n){await i(t.Warn,e,n)},e}({});Object.defineProperty(window.__TAURI__,"log",{value:__TAURI_PLUGIN_LOG__})}
|
if("__TAURI__"in window){var __TAURI_PLUGIN_LOG__=function(e){"use strict";function n(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}async function r(e,n={},r){return window.__TAURI_INTERNALS__.invoke(e,n,r)}var a,t;async function o(e,a,t){const o={kind:"Any"};return r("plugin:event|listen",{event:e,target:o,handler:n(a)}).then((n=>async()=>async function(e,n){await r("plugin:event|unlisten",{event:e,eventId:n})}(e,n)))}async function i(e,n,a){const t=function(e){if(e){if(!e.startsWith("Error"))return e.split("\n").map((e=>e.split("@"))).filter((([e,n])=>e.length>0&&"[native code]"!==n))[2].filter((e=>e.length>0)).join("@");{const n=e.split("\n")[3].trim(),r=/at\s+(?<functionName>.*?)\s+\((?<fileName>.*?):(?<lineNumber>\d+):(?<columnNumber>\d+)\)/,a=n.match(r);if(a){const{functionName:e,fileName:n,lineNumber:r,columnNumber:t}=a.groups;return`${e}@${n}:${r}:${t}`}{const e=/at\s+(?<fileName>.*?):(?<lineNumber>\d+):(?<columnNumber>\d+)/,r=n.match(e);if(r){const{fileName:e,lineNumber:n,columnNumber:a}=r.groups;return`<anonymous>@${e}:${n}:${a}`}}}}}((new Error).stack),{file:o,line:i,keyValues:u}=a??{};await r("plugin:log|log",{level:e,message:n,location:t,file:o,line:i,keyValues:u})}async function u(e){return await o("log://log",(n=>{const{level:r}=n.payload;let{message:a}=n.payload;a=a.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,""),e({message:a,level:r})}))}return"function"==typeof SuppressedError&&SuppressedError,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG_ENTER="tauri://drag-enter",e.DRAG_OVER="tauri://drag-over",e.DRAG_DROP="tauri://drag-drop",e.DRAG_LEAVE="tauri://drag-leave"}(a||(a={})),function(e){e[e.Trace=1]="Trace",e[e.Debug=2]="Debug",e[e.Info=3]="Info",e[e.Warn=4]="Warn",e[e.Error=5]="Error"}(t||(t={})),e.attachConsole=async function(){return await u((({level:e,message:n})=>{switch(e){case t.Trace:console.log(n);break;case t.Debug:console.debug(n);break;case t.Info:console.info(n);break;case t.Warn:console.warn(n);break;case t.Error:console.error(n);break;default:throw new Error(`unknown log level ${e}`)}}))},e.attachLogger=u,e.debug=async function(e,n){await i(t.Debug,e,n)},e.error=async function(e,n){await i(t.Error,e,n)},e.info=async function(e,n){await i(t.Info,e,n)},e.trace=async function(e,n){await i(t.Trace,e,n)},e.warn=async function(e,n){await i(t.Warn,e,n)},e}({});Object.defineProperty(window.__TAURI__,"log",{value:__TAURI_PLUGIN_LOG__})}
|
||||||
|
|||||||
@@ -44,24 +44,78 @@ enum LogLevel {
|
|||||||
Error
|
Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCallerLocation(stack?: string) {
|
||||||
|
if (!stack) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stack.startsWith('Error')) {
|
||||||
|
// Assume it's Chromium V8
|
||||||
|
//
|
||||||
|
// Error
|
||||||
|
// at baz (filename.js:10:15)
|
||||||
|
// at bar (filename.js:6:3)
|
||||||
|
// at foo (filename.js:2:3)
|
||||||
|
// at filename.js:13:1
|
||||||
|
|
||||||
|
const lines = stack.split('\n')
|
||||||
|
// Find the third line (caller's caller of the current location)
|
||||||
|
const callerLine = lines[3].trim()
|
||||||
|
|
||||||
|
const regex =
|
||||||
|
/at\s+(?<functionName>.*?)\s+\((?<fileName>.*?):(?<lineNumber>\d+):(?<columnNumber>\d+)\)/
|
||||||
|
const match = callerLine.match(regex)
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const { functionName, fileName, lineNumber, columnNumber } =
|
||||||
|
match.groups as {
|
||||||
|
functionName: string
|
||||||
|
fileName: string
|
||||||
|
lineNumber: string
|
||||||
|
columnNumber: string
|
||||||
|
}
|
||||||
|
return `${functionName}@${fileName}:${lineNumber}:${columnNumber}`
|
||||||
|
} else {
|
||||||
|
// Handle cases where the regex does not match (e.g., last line without function name)
|
||||||
|
const regexNoFunction =
|
||||||
|
/at\s+(?<fileName>.*?):(?<lineNumber>\d+):(?<columnNumber>\d+)/
|
||||||
|
const matchNoFunction = callerLine.match(regexNoFunction)
|
||||||
|
if (matchNoFunction) {
|
||||||
|
const { fileName, lineNumber, columnNumber } =
|
||||||
|
matchNoFunction.groups as {
|
||||||
|
fileName: string
|
||||||
|
lineNumber: string
|
||||||
|
columnNumber: string
|
||||||
|
}
|
||||||
|
return `<anonymous>@${fileName}:${lineNumber}:${columnNumber}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Assume it's Webkit JavaScriptCore, example:
|
||||||
|
//
|
||||||
|
// baz@filename.js:10:24
|
||||||
|
// bar@filename.js:6:6
|
||||||
|
// foo@filename.js:2:6
|
||||||
|
// global code@filename.js:13:4
|
||||||
|
|
||||||
|
const traces = stack.split('\n').map((line) => line.split('@'))
|
||||||
|
const filtered = traces.filter(([name, location]) => {
|
||||||
|
return name.length > 0 && location !== '[native code]'
|
||||||
|
})
|
||||||
|
// Find the third line (caller's caller of the current location)
|
||||||
|
return filtered[2].filter((v) => v.length > 0).join('@')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function log(
|
async function log(
|
||||||
level: LogLevel,
|
level: LogLevel,
|
||||||
message: string,
|
message: string,
|
||||||
options?: LogOptions
|
options?: LogOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const traces = new Error().stack?.split('\n').map((line) => line.split('@'))
|
const location = getCallerLocation(new Error().stack)
|
||||||
|
|
||||||
const filtered = traces?.filter(([name, location]) => {
|
|
||||||
return name.length > 0 && location !== '[native code]'
|
|
||||||
})
|
|
||||||
|
|
||||||
const { file, line, keyValues } = options ?? {}
|
const { file, line, keyValues } = options ?? {}
|
||||||
|
|
||||||
let location = filtered?.[0]?.filter((v) => v.length > 0).join('@')
|
|
||||||
if (location === 'Error') {
|
|
||||||
location = 'webview::unknown'
|
|
||||||
}
|
|
||||||
|
|
||||||
await invoke('plugin:log|log', {
|
await invoke('plugin:log|log', {
|
||||||
level,
|
level,
|
||||||
message,
|
message,
|
||||||
|
|||||||
+9
-17
@@ -2,8 +2,6 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//! [](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/log)
|
|
||||||
//!
|
|
||||||
//! Logging for Tauri applications.
|
//! Logging for Tauri applications.
|
||||||
|
|
||||||
#![doc(
|
#![doc(
|
||||||
@@ -33,7 +31,7 @@ use tauri::{AppHandle, Emitter};
|
|||||||
pub use fern;
|
pub use fern;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
pub const WEBVIEW_TARGET: &str = "Webview";
|
pub const WEBVIEW_TARGET: &str = "webview";
|
||||||
|
|
||||||
#[cfg(target_os = "ios")]
|
#[cfg(target_os = "ios")]
|
||||||
mod ios {
|
mod ios {
|
||||||
@@ -230,22 +228,16 @@ fn log(
|
|||||||
line: Option<u32>,
|
line: Option<u32>,
|
||||||
key_values: Option<HashMap<String, String>>,
|
key_values: Option<HashMap<String, String>>,
|
||||||
) {
|
) {
|
||||||
let location = location.unwrap_or("webview");
|
|
||||||
|
|
||||||
let level = log::Level::from(level);
|
let level = log::Level::from(level);
|
||||||
|
|
||||||
let metadata = log::MetadataBuilder::new()
|
let target = if let Some(location) = location {
|
||||||
.level(level)
|
format!("{WEBVIEW_TARGET}:{location}")
|
||||||
.target(WEBVIEW_TARGET)
|
} else {
|
||||||
.build();
|
WEBVIEW_TARGET.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
let mut builder = RecordBuilder::new();
|
let mut builder = RecordBuilder::new();
|
||||||
builder
|
builder.level(level).target(&target).file(file).line(line);
|
||||||
.level(level)
|
|
||||||
.metadata(metadata)
|
|
||||||
.target(location)
|
|
||||||
.file(file)
|
|
||||||
.line(line);
|
|
||||||
|
|
||||||
let key_values = key_values.unwrap_or_default();
|
let key_values = key_values.unwrap_or_default();
|
||||||
let mut kv = HashMap::new();
|
let mut kv = HashMap::new();
|
||||||
@@ -380,8 +372,8 @@ impl Builder {
|
|||||||
/// .clear_targets()
|
/// .clear_targets()
|
||||||
/// .targets([
|
/// .targets([
|
||||||
/// Target::new(TargetKind::Webview),
|
/// Target::new(TargetKind::Webview),
|
||||||
/// Target::new(TargetKind::LogDir { file_name: Some("webview".into()) }).filter(|metadata| metadata.target() == WEBVIEW_TARGET),
|
/// Target::new(TargetKind::LogDir { file_name: Some("webview".into()) }).filter(|metadata| metadata.target().starts_with(WEBVIEW_TARGET)),
|
||||||
/// Target::new(TargetKind::LogDir { file_name: Some("rust".into()) }).filter(|metadata| metadata.target() != WEBVIEW_TARGET),
|
/// Target::new(TargetKind::LogDir { file_name: Some("rust".into()) }).filter(|metadata| !metadata.target().starts_with(WEBVIEW_TARGET)),
|
||||||
/// ]);
|
/// ]);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn targets(mut self, targets: impl IntoIterator<Item = Target>) -> Self {
|
pub fn targets(mut self, targets: impl IntoIterator<Item = Target>) -> Self {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ rustdoc-args = ["--cfg", "docsrs"]
|
|||||||
targets = ["x86_64-unknown-linux-gnu", "x86_64-linux-android"]
|
targets = ["x86_64-unknown-linux-gnu", "x86_64-linux-android"]
|
||||||
|
|
||||||
[package.metadata.platforms.support]
|
[package.metadata.platforms.support]
|
||||||
windows = { level = "full", notes = "" }
|
windows = { level = "full", notes = "Only works for installed apps. Shows powershell name & icon in development." }
|
||||||
linux = { level = "full", notes = "" }
|
linux = { level = "full", notes = "" }
|
||||||
macos = { level = "full", notes = "" }
|
macos = { level = "full", notes = "" }
|
||||||
android = { level = "full", notes = "" }
|
android = { level = "full", notes = "" }
|
||||||
|
|||||||
@@ -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}|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__})}
|
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;"function"==typeof SuppressedError&&SuppressedError;const r="__TAURI_TO_IPC_KEY__";class c{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")}[(e=new WeakMap,o=new WeakMap,a=new WeakMap,r)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[r]()}}class s{constructor(i,t,n){this.plugin=i,this.event=t,this.channelId=n}async unregister(){return u(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}}async function l(i,t,n){const e=new c;return e.onmessage=n,u(`plugin:${i}|registerListener`,{event:t,handler:e}).then((()=>new s(i,t,e.id)))}async function u(i,t={},n){return window.__TAURI_INTERNALS__.invoke(i,t,n)}var f,d,w;i.ScheduleEvery=void 0,(f=i.ScheduleEvery||(i.ScheduleEvery={})).Year="year",f.Month="month",f.TwoWeeks="twoWeeks",f.Week="week",f.Day="day",f.Hour="hour",f.Minute="minute",f.Second="second";return i.Importance=void 0,(d=i.Importance||(i.Importance={}))[d.None=0]="None",d[d.Min=1]="Min",d[d.Low=2]="Low",d[d.Default=3]="Default",d[d.High=4]="High",i.Visibility=void 0,(w=i.Visibility||(i.Visibility={}))[w.Secret=-1]="Secret",w[w.Private=0]="Private",w[w.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 u("plugin:notification|get_active")},i.cancel=async function(i){await u("plugin:notification|cancel",{notifications:i})},i.cancelAll=async function(){await u("plugin:notification|cancel")},i.channels=async function(){return await u("plugin:notification|listChannels")},i.createChannel=async function(i){await u("plugin:notification|create_channel",{...i})},i.isPermissionGranted=async function(){return"default"!==window.Notification.permission?await Promise.resolve("granted"===window.Notification.permission):await u("plugin:notification|is_permission_granted")},i.onAction=async function(i){return await l("notification","actionPerformed",i)},i.onNotificationReceived=async function(i){return await l("notification","notification",i)},i.pending=async function(){return await u("plugin:notification|get_pending")},i.registerActionTypes=async function(i){await u("plugin:notification|register_action_types",{types:i})},i.removeActive=async function(i){await u("plugin:notification|remove_active",{notifications:i})},i.removeAllActive=async function(){await u("plugin:notification|remove_active")},i.removeChannel=async function(i){await u("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__})}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//! [](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/notification)
|
|
||||||
//!
|
|
||||||
//! Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API.
|
//! Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API.
|
||||||
|
|
||||||
#![doc(
|
#![doc(
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
[package]
|
||||||
|
name = "tauri-plugin-opener"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Open files and URLs using their default application."
|
||||||
|
edition = { workspace = true }
|
||||||
|
authors = { workspace = true }
|
||||||
|
license = { workspace = true }
|
||||||
|
repository = { workspace = true }
|
||||||
|
links = "tauri-plugin-opener"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
rustc-args = ["--cfg", "docsrs"]
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
|
# Platforms supported by the plugin
|
||||||
|
# Support levels are "full", "partial", "none", "unknown"
|
||||||
|
# Details of the support level are left to plugin maintainer
|
||||||
|
[package.metadata.platforms]
|
||||||
|
windows = { level = "full", notes = "" }
|
||||||
|
linux = { level = "full", notes = "" }
|
||||||
|
macos = { level = "full", notes = "" }
|
||||||
|
android = { level = "partial", notes = "Only allows to open URLs via `open`" }
|
||||||
|
ios = { level = "partial", notes = "Only allows to open URLs via `open`" }
|
||||||
|
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-plugin = { workspace = true, features = ["build"] }
|
||||||
|
schemars = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
tauri = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
open = { version = "5", features = ["shellexecute-on-windows"] }
|
||||||
|
glob = { workspace = true }
|
||||||
|
|
||||||
|
[target."cfg(windows)".dependencies]
|
||||||
|
dunce = { workspace = true }
|
||||||
|
|
||||||
|
[target."cfg(windows)".dependencies.windows]
|
||||||
|
version = "0.58"
|
||||||
|
features = [
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_UI_Shell_Common",
|
||||||
|
"Win32_UI_WindowsAndMessaging",
|
||||||
|
"Win32_System_Com",
|
||||||
|
"Win32_System_Registry",
|
||||||
|
]
|
||||||
|
|
||||||
|
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"netbsd\", target_os = \"openbsd\"))".dependencies]
|
||||||
|
zbus = { workspace = true }
|
||||||
|
url = { workspace = true }
|
||||||
|
|
||||||
|
[target."cfg(target_os = \"macos\")".dependencies.objc2-app-kit]
|
||||||
|
version = "0.2"
|
||||||
|
features = ["NSWorkspace"]
|
||||||
|
|
||||||
|
[target."cfg(target_os = \"macos\")".dependencies.objc2-foundation]
|
||||||
|
version = "0.2"
|
||||||
|
features = ["NSURL", "NSArray", "NSString"]
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "ios")'.dependencies]
|
||||||
|
tauri = { workspace = true, features = ["wry"] }
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
SPDXVersion: SPDX-2.1
|
||||||
|
DataLicense: CC0-1.0
|
||||||
|
PackageName: tauri
|
||||||
|
DataFormat: SPDXRef-1
|
||||||
|
PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy
|
||||||
|
PackageHomePage: https://tauri.app
|
||||||
|
PackageLicenseDeclared: Apache-2.0
|
||||||
|
PackageLicenseDeclared: MIT
|
||||||
|
PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy
|
||||||
|
PackageSummary: <text>Tauri is a rust project that enables developers to make secure
|
||||||
|
and small desktop applications using a web frontend.
|
||||||
|
</text>
|
||||||
|
PackageComment: <text>The package includes the following libraries; see
|
||||||
|
Relationship information.
|
||||||
|
</text>
|
||||||
|
Created: 2019-05-20T09:00:00Z
|
||||||
|
PackageDownloadLocation: git://github.com/tauri-apps/tauri
|
||||||
|
PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git
|
||||||
|
PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git
|
||||||
|
Creator: Person: Daniel Thompson-Yvetot
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 - Present Tauri Apps Contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|

|
||||||
|
|
||||||
|
<!-- description -->
|
||||||
|
|
||||||
|
| Platform | Supported |
|
||||||
|
| -------- | --------- |
|
||||||
|
| Linux | ✓ |
|
||||||
|
| Windows | ✓ |
|
||||||
|
| macOS | ✓ |
|
||||||
|
| Android | ? |
|
||||||
|
| iOS | ? |
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
_This plugin requires a Rust version of at least **1.77.2**_
|
||||||
|
|
||||||
|
There are three general methods of installation that we can recommend.
|
||||||
|
|
||||||
|
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)
|
||||||
|
2. Pull sources directly from Github using git tags / revision hashes (most secure)
|
||||||
|
3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use)
|
||||||
|
|
||||||
|
Install the Core plugin by adding the following to your `Cargo.toml` file:
|
||||||
|
|
||||||
|
`src-tauri/Cargo.toml`
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
tauri-plugin-opener = "2.0.0"
|
||||||
|
# alternatively with Git:
|
||||||
|
tauri-plugin-opener = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||||
|
```
|
||||||
|
|
||||||
|
You can install the JavaScript Guest bindings using your preferred JavaScript package manager:
|
||||||
|
|
||||||
|
> Note: Since most JavaScript package managers are unable to install packages from git monorepos we provide read-only mirrors of each plugin. This makes installation option 2 more ergonomic to use.
|
||||||
|
|
||||||
|
<!-- Add the branch for installations using git! -->
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm add @tauri-apps/plugin-opener
|
||||||
|
# or
|
||||||
|
npm add @tauri-apps/plugin-opener
|
||||||
|
# or
|
||||||
|
yarn add @tauri-apps/plugin-opener
|
||||||
|
|
||||||
|
# alternatively with Git:
|
||||||
|
pnpm add https://github.com/tauri-apps/tauri-plugin-opener#v2
|
||||||
|
# or
|
||||||
|
npm add https://github.com/tauri-apps/tauri-plugin-opener#v2
|
||||||
|
# or
|
||||||
|
yarn add https://github.com/tauri-apps/tauri-plugin-opener#v2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
First you need to register the core plugin with Tauri:
|
||||||
|
|
||||||
|
`src-tauri/src/main.rs`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn main() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Afterwards all the plugin's APIs are available through the JavaScript guest bindings:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
PRs accepted. Please make sure to read the Contributing Guide before making a pull request.
|
||||||
|
|
||||||
|
## Partners
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://crabnebula.dev" target="_blank">
|
||||||
|
<img src="https://github.com/tauri-apps/plugins-workspace/raw/v2/.github/sponsors/crabnebula.svg" alt="CrabNebula" width="283">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy.
|
||||||
|
|
||||||
|
MIT or MIT/Apache 2.0 where applicable.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
**Do not report security vulnerabilities through public GitHub issues.**
|
||||||
|
|
||||||
|
**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.**
|
||||||
|
|
||||||
|
Include as much of the following information:
|
||||||
|
|
||||||
|
- Type of issue (e.g. improper input parsing, privilege escalation, etc.)
|
||||||
|
- The location of the affected source code (tag/branch/commit or direct URL)
|
||||||
|
- Any special configuration required to reproduce the issue
|
||||||
|
- The distribution affected or used to help us with reproduction of the issue
|
||||||
|
- Step-by-step instructions to reproduce the issue
|
||||||
|
- Ideally a reproduction repository
|
||||||
|
- Impact of the issue, including how an attacker might exploit the issue
|
||||||
|
|
||||||
|
We prefer to receive reports in English.
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new).
|
||||||
|
|
||||||
|
Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app).
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/build
|
||||||
|
/.tauri
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "app.tauri.opener"
|
||||||
|
compileSdk = 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
consumerProguardFiles("consumer-rules.pro")
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("androidx.core:core-ktx:1.9.0")
|
||||||
|
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3")
|
||||||
|
implementation(project(":tauri-android"))
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
include ':tauri-android'
|
||||||
|
project(':tauri-android').projectDir = new File('./.tauri/tauri-api')
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package app.tauri.shell
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import app.tauri.annotation.Command
|
||||||
|
import app.tauri.annotation.TauriPlugin
|
||||||
|
import app.tauri.plugin.Invoke
|
||||||
|
import app.tauri.plugin.Plugin
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@TauriPlugin
|
||||||
|
class OpenerPlugin(private val activity: Activity) : Plugin(activity) {
|
||||||
|
@Command
|
||||||
|
fun open(invoke: Invoke) {
|
||||||
|
try {
|
||||||
|
val url = invoke.parseArgs(String::class.java)
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
activity.applicationContext?.startActivity(intent)
|
||||||
|
invoke.resolve()
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
invoke.reject(ex.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
if("__TAURI__"in window){var __TAURI_PLUGIN_OPENER__=function(n){"use strict";async function e(n,e={},_){return window.__TAURI_INTERNALS__.invoke(n,e,_)}return"function"==typeof SuppressedError&&SuppressedError,n.openPath=async function(n,_){await e("plugin:opener|open_path",{path:n,with:_})},n.openUrl=async function(n,_){await e("plugin:opener|open_url",{url:n,with:_})},n.revealItemInDir=async function(n){return e("plugin:opener|reveal_item_in_dir",{path:n})},n}({});Object.defineProperty(window.__TAURI__,"opener",{value:__TAURI_PLUGIN_OPENER__})}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[path = "src/scope_entry.rs"]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
mod scope;
|
||||||
|
|
||||||
|
/// Opener scope application.
|
||||||
|
#[derive(schemars::JsonSchema)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
#[allow(unused)]
|
||||||
|
enum Application {
|
||||||
|
/// Open in default application.
|
||||||
|
Default,
|
||||||
|
/// If true, allow open with any application.
|
||||||
|
Enable(bool),
|
||||||
|
/// Allow specific application to open with.
|
||||||
|
App(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Application {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opener scope entry.
|
||||||
|
#[derive(schemars::JsonSchema)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
#[allow(unused)]
|
||||||
|
enum OpenerScopeEntry {
|
||||||
|
Url {
|
||||||
|
/// A URL that can be opened by the webview when using the Opener APIs.
|
||||||
|
///
|
||||||
|
/// Wildcards can be used following the UNIX glob pattern.
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
///
|
||||||
|
/// - "https://*" : allows all HTTPS origin
|
||||||
|
///
|
||||||
|
/// - "https://*.github.com/tauri-apps/tauri": allows any subdomain of "github.com" with the "tauri-apps/api" path
|
||||||
|
///
|
||||||
|
/// - "https://myapi.service.com/users/*": allows access to any URLs that begins with "https://myapi.service.com/users/"
|
||||||
|
url: String,
|
||||||
|
/// An application to open this url with, for example: firefox.
|
||||||
|
#[serde(default)]
|
||||||
|
app: Application,
|
||||||
|
},
|
||||||
|
Path {
|
||||||
|
/// A path that can be opened by the webview when using the Opener APIs.
|
||||||
|
///
|
||||||
|
/// The pattern can start with a variable that resolves to a system base directory.
|
||||||
|
/// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,
|
||||||
|
/// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,
|
||||||
|
/// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,
|
||||||
|
/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
|
||||||
|
path: PathBuf,
|
||||||
|
/// An application to open this path with, for example: xdg-open.
|
||||||
|
#[serde(default)]
|
||||||
|
app: Application,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure `OpenerScopeEntry` and `scope::EntryRaw` is kept in sync
|
||||||
|
fn _f() {
|
||||||
|
match (scope::EntryRaw::Url {
|
||||||
|
url: String::new(),
|
||||||
|
app: scope::Application::Enable(true),
|
||||||
|
}) {
|
||||||
|
scope::EntryRaw::Url { url, app } => OpenerScopeEntry::Url {
|
||||||
|
url,
|
||||||
|
app: match app {
|
||||||
|
scope::Application::Enable(p) => Application::Enable(p),
|
||||||
|
scope::Application::App(p) => Application::App(p),
|
||||||
|
scope::Application::Default => Application::Default,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scope::EntryRaw::Path { path, app } => OpenerScopeEntry::Path {
|
||||||
|
path,
|
||||||
|
app: match app {
|
||||||
|
scope::Application::Enable(p) => Application::Enable(p),
|
||||||
|
scope::Application::App(p) => Application::App(p),
|
||||||
|
scope::Application::Default => Application::Default,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
match (OpenerScopeEntry::Url {
|
||||||
|
url: String::new(),
|
||||||
|
app: Application::Enable(true),
|
||||||
|
}) {
|
||||||
|
OpenerScopeEntry::Url { url, app } => scope::EntryRaw::Url {
|
||||||
|
url,
|
||||||
|
app: match app {
|
||||||
|
Application::Enable(p) => scope::Application::Enable(p),
|
||||||
|
Application::App(p) => scope::Application::App(p),
|
||||||
|
Application::Default => scope::Application::Default,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
OpenerScopeEntry::Path { path, app } => scope::EntryRaw::Path {
|
||||||
|
path,
|
||||||
|
app: match app {
|
||||||
|
Application::Enable(p) => scope::Application::Enable(p),
|
||||||
|
Application::App(p) => scope::Application::App(p),
|
||||||
|
Application::Default => scope::Application::Default,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const COMMANDS: &[&str] = &["open_url", "open_path", "reveal_item_in_dir"];
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tauri_plugin::Builder::new(COMMANDS)
|
||||||
|
.global_api_script_path("./api-iife.js")
|
||||||
|
.android_path("android")
|
||||||
|
.ios_path("ios")
|
||||||
|
.global_scope_schema(schemars::schema_for!(OpenerScopeEntry))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||||
|
let mobile = target_os == "ios" || target_os == "android";
|
||||||
|
alias("desktop", !mobile);
|
||||||
|
alias("mobile", mobile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates a cfg alias if `has_feature` is true.
|
||||||
|
// `alias` must be a snake case string.
|
||||||
|
fn alias(alias: &str, has_feature: bool) {
|
||||||
|
println!("cargo:rustc-check-cfg=cfg({alias})");
|
||||||
|
if has_feature {
|
||||||
|
println!("cargo:rustc-cfg={alias}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open files and URLs using their default application.
|
||||||
|
*
|
||||||
|
* ## Security
|
||||||
|
*
|
||||||
|
* This API has a scope configuration that forces you to restrict the files and urls to be opened.
|
||||||
|
*
|
||||||
|
* ### Restricting access to the {@link open | `open`} API
|
||||||
|
*
|
||||||
|
* On the configuration object, `open: true` means that the {@link open} API can be used with any URL,
|
||||||
|
* as the argument is validated with the `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+` regex.
|
||||||
|
* You can change that regex by changing the boolean value to a string, e.g. `open: ^https://github.com/`.
|
||||||
|
*
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a url with the system's default app, or the one specified with {@linkcode openWith}.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { openUrl } from '@tauri-apps/plugin-opener';
|
||||||
|
*
|
||||||
|
* // opens the given URL on the default browser:
|
||||||
|
* await openUrl('https://github.com/tauri-apps/tauri');
|
||||||
|
* // opens the given URL using `firefox`:
|
||||||
|
* await openUrl('https://github.com/tauri-apps/tauri', 'firefox');
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param url The URL to open.
|
||||||
|
* @param openWith The app to open the URL with. If not specified, defaults to the system default application for the specified url type.
|
||||||
|
*
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
export async function openUrl(url: string, openWith?: string): Promise<void> {
|
||||||
|
await invoke('plugin:opener|open_url', {
|
||||||
|
url,
|
||||||
|
with: openWith
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a path with the system's default app, or the one specified with {@linkcode openWith}.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { openPath } from '@tauri-apps/plugin-opener';
|
||||||
|
*
|
||||||
|
* // opens a file using the default program:
|
||||||
|
* await openPath('/path/to/file');
|
||||||
|
* // opens a file using `vlc` command on Windows.
|
||||||
|
* await openPath('C:/path/to/file', 'vlc');
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param path The path to open.
|
||||||
|
* @param openWith The app to open the path with. If not specified, defaults to the system default application for the specified path type.
|
||||||
|
*
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
export async function openPath(path: string, openWith?: string): Promise<void> {
|
||||||
|
await invoke('plugin:opener|open_path', {
|
||||||
|
path,
|
||||||
|
with: openWith
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reveal a path the system's default explorer.
|
||||||
|
*
|
||||||
|
* #### Platform-specific:
|
||||||
|
*
|
||||||
|
* - **Android / iOS:** Unsupported.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { revealItemInDir } from '@tauri-apps/plugin-opener';
|
||||||
|
* await revealItemInDir('/path/to/file');
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param path The path to reveal.
|
||||||
|
*
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
export async function revealItemInDir(path: string) {
|
||||||
|
return invoke('plugin:opener|reveal_item_in_dir', { path })
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
|
// open <a href="..."> links with the API
|
||||||
|
window.addEventListener('click', function (evt) {
|
||||||
|
// return early if
|
||||||
|
if (
|
||||||
|
// event was prevented
|
||||||
|
evt.defaultPrevented ||
|
||||||
|
// or not a left click
|
||||||
|
evt.button !== 0 ||
|
||||||
|
// or meta key pressed
|
||||||
|
evt.metaKey ||
|
||||||
|
// or al key pressed
|
||||||
|
evt.altKey
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
const a = evt
|
||||||
|
.composedPath()
|
||||||
|
.find((el) => el instanceof Node && el.nodeName.toUpperCase() === 'A') as
|
||||||
|
| HTMLAnchorElement
|
||||||
|
| undefined
|
||||||
|
|
||||||
|
// return early if
|
||||||
|
if (
|
||||||
|
// not tirggered from <a> element
|
||||||
|
!a ||
|
||||||
|
// or doesn't have a href
|
||||||
|
!a.href ||
|
||||||
|
// or not supposed to be open in a new tab
|
||||||
|
!(
|
||||||
|
a.target === '_blank' ||
|
||||||
|
// or ctrl key pressed
|
||||||
|
evt.ctrlKey ||
|
||||||
|
// or shift key pressed
|
||||||
|
evt.shiftKey
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
const url = new URL(a.href)
|
||||||
|
|
||||||
|
// return early if
|
||||||
|
if (
|
||||||
|
// same origin (internal navigation)
|
||||||
|
url.origin === window.location.origin ||
|
||||||
|
// not default protocols
|
||||||
|
['http:', 'https:', 'mailto:', 'tel:'].every((p) => url.protocol !== p)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
evt.preventDefault()
|
||||||
|
|
||||||
|
void invoke('plugin:opener|open_url', {
|
||||||
|
url
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"object": {
|
||||||
|
"pins": [
|
||||||
|
{
|
||||||
|
"package": "SwiftRs",
|
||||||
|
"repositoryURL": "https://github.com/Brendonovich/swift-rs",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "b5ed223fcdab165bc21219c1925dc1e77e2bef5e",
|
||||||
|
"version": "1.0.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// swift-tools-version:5.3
|
||||||
|
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "tauri-plugin-opener",
|
||||||
|
platforms: [
|
||||||
|
.macOS(.v10_13),
|
||||||
|
.iOS(.v13),
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||||
|
.library(
|
||||||
|
name: "tauri-plugin-opener",
|
||||||
|
type: .static,
|
||||||
|
targets: ["tauri-plugin-opener"])
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
.package(name: "Tauri", path: "../.tauri/tauri-api")
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||||
|
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||||
|
.target(
|
||||||
|
name: "tauri-plugin-opener",
|
||||||
|
dependencies: [
|
||||||
|
.byName(name: "Tauri")
|
||||||
|
],
|
||||||
|
path: "Sources")
|
||||||
|
]
|
||||||
|
)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
import SwiftRs
|
||||||
|
import Tauri
|
||||||
|
import UIKit
|
||||||
|
import WebKit
|
||||||
|
|
||||||
|
class OpenerPlugin: Plugin {
|
||||||
|
|
||||||
|
@objc public func open(_ invoke: Invoke) throws {
|
||||||
|
do {
|
||||||
|
let urlString = try invoke.parseArgs(String.self)
|
||||||
|
if let url = URL(string: urlString) {
|
||||||
|
if #available(iOS 10, *) {
|
||||||
|
UIApplication.shared.open(url, options: [:])
|
||||||
|
} else {
|
||||||
|
UIApplication.shared.openURL(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invoke.resolve()
|
||||||
|
} catch {
|
||||||
|
invoke.reject(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@_cdecl("init_plugin_shell")
|
||||||
|
func initPlugin() -> Plugin {
|
||||||
|
return OpenerPlugin()
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "@tauri-apps/plugin-opener",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Open files and URLs using their default application.",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"authors": [
|
||||||
|
"Tauri Programme within The Commons Conservancy"
|
||||||
|
],
|
||||||
|
"repository": "https://github.com/tauri-apps/plugins-workspace",
|
||||||
|
"type": "module",
|
||||||
|
"types": "./dist-js/index.d.ts",
|
||||||
|
"main": "./dist-js/index.cjs",
|
||||||
|
"module": "./dist-js/index.js",
|
||||||
|
"exports": {
|
||||||
|
"types": "./dist-js/index.d.ts",
|
||||||
|
"import": "./dist-js/index.js",
|
||||||
|
"require": "./dist-js/index.cjs"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "rollup -c"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist-js",
|
||||||
|
"README.md",
|
||||||
|
"LICENSE"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
"$schema" = "schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-default-urls"
|
||||||
|
description = "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application."
|
||||||
|
|
||||||
|
[[permission.scope.allow]]
|
||||||
|
url = "mailto:*"
|
||||||
|
|
||||||
|
[[permission.scope.allow]]
|
||||||
|
url = "tel:*"
|
||||||
|
|
||||||
|
[[permission.scope.allow]]
|
||||||
|
url = "http://*"
|
||||||
|
|
||||||
|
[[permission.scope.allow]]
|
||||||
|
url = "https://*"
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-open-path"
|
||||||
|
description = "Enables the open_path command without any pre-configured scope."
|
||||||
|
commands.allow = ["open_path"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-open-path"
|
||||||
|
description = "Denies the open_path command without any pre-configured scope."
|
||||||
|
commands.deny = ["open_path"]
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-open-url"
|
||||||
|
description = "Enables the open_url command without any pre-configured scope."
|
||||||
|
commands.allow = ["open_url"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-open-url"
|
||||||
|
description = "Denies the open_url command without any pre-configured scope."
|
||||||
|
commands.deny = ["open_url"]
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-reveal-item-in-dir"
|
||||||
|
description = "Enables the reveal_item_in_dir command without any pre-configured scope."
|
||||||
|
commands.allow = ["reveal_item_in_dir"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-reveal-item-in-dir"
|
||||||
|
description = "Denies the reveal_item_in_dir command without any pre-configured scope."
|
||||||
|
commands.deny = ["reveal_item_in_dir"]
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
## Default Permission
|
||||||
|
|
||||||
|
This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application
|
||||||
|
as well as reveal file in directories using default file explorer
|
||||||
|
|
||||||
|
- `allow-open-url`
|
||||||
|
- `allow-reveal-item-in-dir`
|
||||||
|
- `allow-default-urls`
|
||||||
|
|
||||||
|
## Permission Table
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Identifier</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`opener:allow-default-urls`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`opener:allow-open-path`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Enables the open_path command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`opener:deny-open-path`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Denies the open_path command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`opener:allow-open-url`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Enables the open_url command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`opener:deny-open-url`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Denies the open_url command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`opener:allow-reveal-item-in-dir`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Enables the reveal_item_in_dir command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`opener:deny-reveal-item-in-dir`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Denies the reveal_item_in_dir command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
"$schema" = "schemas/schema.json"
|
||||||
|
|
||||||
|
[default]
|
||||||
|
description = """This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application
|
||||||
|
as well as reveal file in directories using default file explorer"""
|
||||||
|
permissions = [
|
||||||
|
"allow-open-url",
|
||||||
|
"allow-reveal-item-in-dir",
|
||||||
|
"allow-default-urls",
|
||||||
|
]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user