mirror of https://github.com/tauri-apps/tauri
Merge remote-tracking branch 'origin/dev' into feat/command-tests
This commit is contained in:
commit
83a9e6c572
|
@ -0,0 +1,2 @@
|
|||
[env]
|
||||
__TAURI_WORKSPACE__ = "true"
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"tauri-bundler": patch
|
||||
---
|
||||
|
||||
Add dylib support to `tauri.bundle.macOS.frameworks`.
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"api": minor
|
||||
---
|
||||
|
||||
Added the `additionalBrowserArgs` option when creating a window.
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"tauri-utils": minor
|
||||
---
|
||||
|
||||
Added the `additional_browser_args` option to the window configuration.
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
"tauri": minor
|
||||
"tauri-runtime-wry": minor
|
||||
"tauri-runtime": minor
|
||||
---
|
||||
|
||||
Added the `additional_browser_args` option when creating a window.
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"cli.rs": "patch"
|
||||
---
|
||||
|
||||
Fix `tauri info` panicking when parsing crates version on a newly created project without a `Cargo.lock` file.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"api": "minor"
|
||||
---
|
||||
|
||||
Added the `WindowOptions::contentProtected` option and `WebviewWindow#setContentProtected` to change it at runtime.
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"tauri-utils": "patch"
|
||||
---
|
||||
|
||||
Added the `content_protected` option to the window configuration.
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
"tauri": "minor"
|
||||
"tauri-runtime": "minor"
|
||||
"tauri-runtime-wry": "minor"
|
||||
---
|
||||
|
||||
Added the `content_protected` option when creating a window and `Window::set_content_protected` to change it at runtime.
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"tauri": minor
|
||||
---
|
||||
|
||||
Added `Window::on_navigation`.
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"tauri": "patch"
|
||||
---
|
||||
|
||||
Sync `__TAURI_METADATA__.__windows` across all windows.
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"api": minor
|
||||
---
|
||||
|
||||
Allow setting the text of the dialog buttons.
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"tauri": minor
|
||||
---
|
||||
|
||||
Added `OkWithLabel` and `OkCancelWithLabels` variants to the `api::dialog::MessageDialogButtons` enum to set the text of the dialog buttons.
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"cli.rs": minor
|
||||
"cli.js": minor
|
||||
---
|
||||
|
||||
Add `--png` option for the `icon` command to generate custom icon sizes.
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
"tauri-runtime-wry": minor
|
||||
"tauri-runtime": minor
|
||||
"tauri": minor
|
||||
---
|
||||
|
||||
Added `Builder::device_event_filter` and `App::set_device_event_filter` methods.
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"tauri": "patch"
|
||||
---
|
||||
|
||||
Fix resize glitch when double clicking a custom titlebar in the top resize area.
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"tauri": "patch"
|
||||
---
|
||||
|
||||
Fix the filesystem scope allowing sub-directories of the directory picked by the dialog when `recursive` option was `false`.
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"cli.rs": "patch"
|
||||
---
|
||||
|
||||
Fix building apps with unicode characters in their `productName`.
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
"tauri": minor
|
||||
"tauri-runtime": minor
|
||||
"tauri-runtime-wry": minor
|
||||
"api": minor
|
||||
---
|
||||
|
||||
Add `is_minimized()` window method.
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
"tauri-bundler": minor
|
||||
"tauri-utils": minor
|
||||
"cli.rs": minor
|
||||
"cli.js": minor
|
||||
---
|
||||
|
||||
Add `nsis` bundle target
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"tauri": minor
|
||||
---
|
||||
|
||||
Added support to `mailto:` and `tel:` links on the shell API.
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"tauri-runtime": minor
|
||||
---
|
||||
|
||||
Added `navigation_handler` field on `PendingWindow`.
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"tauri-build": "minor"
|
||||
---
|
||||
|
||||
Add `WindowsAttributes::app_manifest` to specify the application manifest on Windows.
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
"tauri": "minor"
|
||||
"api": "minor"
|
||||
"tauri-runtime": "minor"
|
||||
"tauri-runtime-wry": "minor"
|
||||
---
|
||||
|
||||
Add `title` getter on window.
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"tauri-runtime": minor
|
||||
"tauri-runtime-wry": minor
|
||||
---
|
||||
|
||||
Added `TrayHandle::set_tooltip` and `SystemTray::with_tooltip`.
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"tauri": patch
|
||||
---
|
||||
|
||||
Implement `SystemTray::with_tooltip` and `SystemTrayHandle::set_tooltip` for Windows and macOS.
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
"tauri-runtime": minor
|
||||
"tauri-runtime-wry": minor
|
||||
"tauri": minor
|
||||
---
|
||||
|
||||
Added window's `url()` getter.
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'cli.rs': 'minor'
|
||||
---
|
||||
|
||||
Add support for Cargo's workspace inheritance for the package version.
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
"tauri": "patch"
|
||||
"tauri-runtime": "patch"
|
||||
"tauri-runtime-wry": "patch"
|
||||
---
|
||||
|
||||
On Windows, change webview theme based on Window theme for more accurate `prefers-color-scheme` support.
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"tauri-bundler": "patch"
|
||||
---
|
||||
|
||||
On Windows, the `msi` installer's `Launch App` checkbox will be checked by default.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"tauri-runtime-wry": minor
|
||||
---
|
||||
|
||||
Implement the webview navigation handler.
|
|
@ -10,6 +10,7 @@ on:
|
|||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_PROFILE_DEV_DEBUG: 0 # This would add unnecessary bloat to the target folder, decreasing cache efficiency.
|
||||
LC_ALL: en_US.UTF-8 # This prevents strace from changing it's number format to use commas.
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
|
|
@ -5,6 +5,14 @@
|
|||
name: Check generated files
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/check-generated-files.yml'
|
||||
- 'tooling/api/src/**'
|
||||
- 'core/tauri/scripts/bundle.global.js'
|
||||
- 'core/tauri-utils/src/config.rs'
|
||||
- 'tooling/cli/schema.json'
|
||||
- 'core/config-schema/schema.json'
|
||||
push:
|
||||
paths:
|
||||
- '.github/workflows/check-generated-files.yml'
|
||||
|
|
|
@ -124,7 +124,7 @@ jobs:
|
|||
token: ${{ secrets.ORG_TAURI_BOT_PAT }}
|
||||
repository: tauri-apps/tauri
|
||||
event-type: publish-clijs
|
||||
inputs: '{"releaseId": "${{ steps.covector.outputs.cli.js-releaseId }}" }'
|
||||
client-payload: '{"releaseId": "${{ steps.covector.outputs.cli.js-releaseId }}" }'
|
||||
|
||||
- name: Trigger cli.rs publishing workflow
|
||||
if: |
|
||||
|
|
|
@ -397,4 +397,4 @@ jobs:
|
|||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.ORG_NPM_TOKEN }}
|
||||
RELEASE_ID: ${{ github.event.inputs.releaseId }}
|
||||
RELEASE_ID: ${{ github.event.client_payload.releaseId || github.event.inputs.releaseId }}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
declare -a examples=("api" "sidecar" "updater" "resources" "tauri-dynamic-lib")
|
||||
declare -a examples=("api" "sidecar" "updater" "resources" "tauri-dynamic-lib" "workspace")
|
||||
declare -a tooling=("bench" "cli" "webdriver")
|
||||
|
||||
for example in "${examples[@]}"
|
||||
|
|
|
@ -21,7 +21,8 @@ exclude = [
|
|||
"examples/updater/src-tauri",
|
||||
"examples/resources/src-tauri",
|
||||
"examples/sidecar/src-tauri",
|
||||
"examples/web/core"
|
||||
"examples/web/core",
|
||||
"examples/workspace"
|
||||
]
|
||||
|
||||
# default to small, optimized workspace release binaries
|
||||
|
|
11
SECURITY.md
11
SECURITY.md
|
@ -13,7 +13,16 @@ If you have found a potential security threat, vulnerability or exploit in Tauri
|
|||
or one of its upstream dependencies, please DON’T create a pull-request, DON’T
|
||||
file an issue on GitHub, DON’T mention it on Discord and DON’T create a forum thread.
|
||||
|
||||
We will be adding contact information to this page very soon.
|
||||
Please submit your report via the GitHub Private Vulnerability Disclosure functionality.
|
||||
|
||||
Find out more about the reporting process [here](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability).
|
||||
|
||||
Our team will triage your report and keep you informed about the progress.
|
||||
We may ask questions or request further guidance on reproduction of the vulnerability in the comments of the advisory, which will be publicized.
|
||||
|
||||
Additionally, we may ask you to independently verify our patch, which will be available in the private advisory branch. Please do not publish your vulnerability during the process or before coordinated public disclosure from our side. We try to adhere to common standards of publication within 90-Days of disclosure.
|
||||
|
||||
Depending on your decision to accept or deny credit for the vulnerability, you will be publicly attributed to the vulnerability and may be mentioned in our announcements.
|
||||
|
||||
At the current time we do not have the financial ability to reward bounties,
|
||||
but in extreme cases will at our discretion consider a reward.
|
||||
|
|
|
@ -18,7 +18,7 @@ pub fn main() -> Result<(), Box<dyn Error>> {
|
|||
crate_dir.join("../../tooling/cli/schema.json"),
|
||||
] {
|
||||
let mut schema_file = BufWriter::new(File::create(file)?);
|
||||
write!(schema_file, "{}", schema_str)?;
|
||||
write!(schema_file, "{schema_str}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -105,6 +105,7 @@
|
|||
"print": false,
|
||||
"requestUserAttention": false,
|
||||
"setAlwaysOnTop": false,
|
||||
"setContentProtected": false,
|
||||
"setCursorGrab": false,
|
||||
"setCursorIcon": false,
|
||||
"setCursorPosition": false,
|
||||
|
@ -145,6 +146,7 @@
|
|||
"allowDowngrades": true,
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": null,
|
||||
"nsis": null,
|
||||
"timestampUrl": null,
|
||||
"tsp": false,
|
||||
"webviewFixedRuntimePath": null,
|
||||
|
@ -168,7 +170,8 @@
|
|||
"dialog": true,
|
||||
"pubkey": "",
|
||||
"windows": {
|
||||
"installMode": "passive"
|
||||
"installMode": "passive",
|
||||
"installerArgs": []
|
||||
}
|
||||
},
|
||||
"windows": []
|
||||
|
@ -281,6 +284,7 @@
|
|||
"allowDowngrades": true,
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": null,
|
||||
"nsis": null,
|
||||
"timestampUrl": null,
|
||||
"tsp": false,
|
||||
"webviewFixedRuntimePath": null,
|
||||
|
@ -378,6 +382,7 @@
|
|||
"print": false,
|
||||
"requestUserAttention": false,
|
||||
"setAlwaysOnTop": false,
|
||||
"setContentProtected": false,
|
||||
"setCursorGrab": false,
|
||||
"setCursorIcon": false,
|
||||
"setCursorPosition": false,
|
||||
|
@ -425,7 +430,8 @@
|
|||
"dialog": true,
|
||||
"pubkey": "",
|
||||
"windows": {
|
||||
"installMode": "passive"
|
||||
"installMode": "passive",
|
||||
"installerArgs": []
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
|
@ -641,6 +647,11 @@
|
|||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"contentProtected": {
|
||||
"description": "Prevents the window contents from being captured by other apps.",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"skipTaskbar": {
|
||||
"description": "If `true`, hides the window icon from the taskbar on Windows and Linux.",
|
||||
"default": false,
|
||||
|
@ -682,6 +693,13 @@
|
|||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"additionalBrowserArgs": {
|
||||
"description": "Defines additional browser arguments on Windows. By default wry passes `--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection` so if you use this method, you also need to disable these components by yourself if you want.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
@ -1004,7 +1022,7 @@
|
|||
"type": "boolean"
|
||||
},
|
||||
"targets": {
|
||||
"description": "The bundle targets, currently supports [\"deb\", \"appimage\", \"msi\", \"app\", \"dmg\", \"updater\"] or \"all\".",
|
||||
"description": "The bundle targets, currently supports [\"deb\", \"appimage\", \"nsis\", \"msi\", \"app\", \"dmg\", \"updater\"] or \"all\".",
|
||||
"default": "all",
|
||||
"allOf": [
|
||||
{
|
||||
|
@ -1118,6 +1136,7 @@
|
|||
"allowDowngrades": true,
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": null,
|
||||
"nsis": null,
|
||||
"timestampUrl": null,
|
||||
"tsp": false,
|
||||
"webviewFixedRuntimePath": null,
|
||||
|
@ -1186,6 +1205,13 @@
|
|||
"msi"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "The NSIS bundle (.exe).",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"nsis"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "The macOS application bundle (.app).",
|
||||
"type": "string",
|
||||
|
@ -1370,6 +1396,17 @@
|
|||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"nsis": {
|
||||
"description": "Configuration for the installer generated with NSIS.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/NsisConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
@ -1618,8 +1655,78 @@
|
|||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"NsisConfig": {
|
||||
"description": "Configuration for the Installer bundle using NSIS.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"license": {
|
||||
"description": "The path to the license file to render on the installer.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"headerImage": {
|
||||
"description": "The path to a bitmap file to display on the header of installers pages.\n\nThe recommended dimensions are 150px x 57px.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sidebarImage": {
|
||||
"description": "The path to a bitmap file for the Welcome page and the Finish page.\n\nThe recommended dimensions are 164px x 314px.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"installerIcon": {
|
||||
"description": "The path to an icon file used as the installer icon.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"installMode": {
|
||||
"description": "Whether the installation will be for all users or just the current user.",
|
||||
"default": "currentUser",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/NSISInstallerMode"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"NSISInstallerMode": {
|
||||
"description": "Install Modes for the NSIS installer.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Default mode for the installer.\n\nInstall the app by default in a directory that doesn't require Administrator access.\n\nInstaller metadata will be saved under the `HKCU` registry path.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"currentUser"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Install the app by default in the `Program Files` folder directory requires Administrator access for the installation.\n\nInstaller metadata will be saved under the `HKLM` registry path.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"perMachine"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Combines both modes and allows the user to choose at install time whether to install for the current user or per machine. Note that this mode will require Administrator access even if the user wants to install it for the current user only.\n\nInstaller metadata will be saved under the `HKLM` or `HKCU` registry path based on the user's choice.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"both"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"AllowlistConfig": {
|
||||
"description": "Allowlist configuration.",
|
||||
"description": "Allowlist configuration. The allowlist is a translation of the [Cargo allowlist features](https://docs.rs/tauri/latest/tauri/#cargo-allowlist-features).\n\n# Notes\n\n- Endpoints that don't have their own allowlist option are enabled by default. - There is only \"opt-in\", no \"opt-out\". Setting an option to `false` has no effect.\n\n# Examples\n\n- * [`\"app-all\": true`](https://tauri.app/v1/api/config/#appallowlistconfig.all) will make the [hide](https://tauri.app/v1/api/js/app#hide) endpoint be available regardless of whether `hide` is set to `false` or `true` in the allowlist.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"all": {
|
||||
|
@ -1661,6 +1768,7 @@
|
|||
"print": false,
|
||||
"requestUserAttention": false,
|
||||
"setAlwaysOnTop": false,
|
||||
"setContentProtected": false,
|
||||
"setCursorGrab": false,
|
||||
"setCursorIcon": false,
|
||||
"setCursorPosition": false,
|
||||
|
@ -2011,6 +2119,11 @@
|
|||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"setContentProtected": {
|
||||
"description": "Allows preventing the window contents from being captured by other apps.",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"setSize": {
|
||||
"description": "Allows setting the window size.",
|
||||
"default": false,
|
||||
|
@ -2211,7 +2324,7 @@
|
|||
"description": "Defines the `shell > open` api scope.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "If the shell open API should be enabled.\n\nIf enabled, the default validation regex (`^https?://`) is used.",
|
||||
"description": "If the shell open API should be enabled.\n\nIf enabled, the default validation regex (`^((mailto:\\w+)|(tel:\\w+)|(https?://\\w+)).+`) is used.",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
|
@ -2559,7 +2672,8 @@
|
|||
"windows": {
|
||||
"description": "The Windows configuration for the updater.",
|
||||
"default": {
|
||||
"installMode": "passive"
|
||||
"installMode": "passive",
|
||||
"installerArgs": []
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
|
@ -2579,6 +2693,14 @@
|
|||
"description": "The updater configuration for Windows.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"installerArgs": {
|
||||
"description": "Additional arguments given to the NSIS or WiX installer.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"installMode": {
|
||||
"description": "The installation mode for the update on Windows. Defaults to `passive`.",
|
||||
"default": "passive",
|
||||
|
@ -2602,7 +2724,7 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"description": "The quiet mode means there's no user interaction required. Requires admin privileges if the installer does.",
|
||||
"description": "The quiet mode means there's no user interaction required. Requires admin privileges if the installer does (WiX).",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"quiet"
|
||||
|
|
|
@ -21,10 +21,10 @@ anyhow = "1"
|
|||
quote = { version = "1", optional = true }
|
||||
tauri-codegen = { version = "1.2.1", path = "../tauri-codegen", optional = true }
|
||||
tauri-utils = { version = "1.2.1", path = "../tauri-utils", features = [ "build", "resources" ] }
|
||||
cargo_toml = "0.13"
|
||||
cargo_toml = "0.14"
|
||||
serde_json = "1"
|
||||
heck = "0.4"
|
||||
json-patch = "0.2"
|
||||
json-patch = "0.3"
|
||||
|
||||
[target."cfg(windows)".dependencies]
|
||||
winres = "0.1"
|
||||
|
|
|
@ -86,7 +86,7 @@ impl CodegenContext {
|
|||
pub fn build(self) -> PathBuf {
|
||||
match self.try_build() {
|
||||
Ok(out) => out,
|
||||
Err(error) => panic!("Error found during Codegen::build: {}", error),
|
||||
Err(error) => panic!("Error found during Codegen::build: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,7 +161,7 @@ impl CodegenContext {
|
|||
)
|
||||
})?;
|
||||
|
||||
writeln!(file, "{}", code).with_context(|| {
|
||||
writeln!(file, "{code}").with_context(|| {
|
||||
format!(
|
||||
"Unable to write tokenstream to out file during tauri-build {}",
|
||||
out.display()
|
||||
|
|
|
@ -35,8 +35,8 @@ fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_binaries<'a>(
|
||||
binaries: ResourcePaths<'a>,
|
||||
fn copy_binaries(
|
||||
binaries: ResourcePaths,
|
||||
target_triple: &str,
|
||||
path: &Path,
|
||||
package_name: Option<&String>,
|
||||
|
@ -48,7 +48,7 @@ fn copy_binaries<'a>(
|
|||
.file_name()
|
||||
.expect("failed to extract external binary filename")
|
||||
.to_string_lossy()
|
||||
.replace(&format!("-{}", target_triple), "");
|
||||
.replace(&format!("-{target_triple}"), "");
|
||||
|
||||
if package_name.map_or(false, |n| n == &file_name) {
|
||||
return Err(anyhow::anyhow!(
|
||||
|
@ -90,7 +90,7 @@ fn has_feature(feature: &str) -> bool {
|
|||
// `alias` must be a snake case string.
|
||||
fn cfg_alias(alias: &str, has_feature: bool) {
|
||||
if has_feature {
|
||||
println!("cargo:rustc-cfg={}", alias);
|
||||
println!("cargo:rustc-cfg={alias}");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,6 +110,15 @@ pub struct WindowsAttributes {
|
|||
///
|
||||
/// If it is left unset, it will look up a path in the registry, i.e. HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots
|
||||
sdk_dir: Option<PathBuf>,
|
||||
/// A string containing an [application manifest] to be included with the application on Windows.
|
||||
///
|
||||
/// Defaults to:
|
||||
/// ```ignore
|
||||
#[doc = include_str!("window-app-manifest.xml")]
|
||||
/// ```
|
||||
///
|
||||
/// [application manifest]: https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests
|
||||
app_manifest: Option<String>,
|
||||
}
|
||||
|
||||
impl WindowsAttributes {
|
||||
|
@ -135,6 +144,45 @@ impl WindowsAttributes {
|
|||
self.sdk_dir = Some(sdk_dir.as_ref().into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the Windows app [manifest].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// The following manifest will brand the exe as requesting administrator privileges.
|
||||
/// Thus, everytime it is executed, a Windows UAC dialog will appear.
|
||||
///
|
||||
/// Note that you can move the manifest contents to a separate file and use `include_str!("manifest.xml")`
|
||||
/// instead of the inline string.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// let mut windows = tauri_build::WindowsAttributes::new();
|
||||
/// windows = windows.app_manifest(r#"
|
||||
/// <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
/// <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
/// <security>
|
||||
/// <requestedPrivileges>
|
||||
/// <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
/// </requestedPrivileges>
|
||||
/// </security>
|
||||
/// </trustInfo>
|
||||
/// </assembly>
|
||||
/// "#);
|
||||
/// tauri_build::try_build(
|
||||
/// tauri_build::Attributes::new().windows_attributes(windows)
|
||||
/// ).expect("failed to run build script");
|
||||
/// ```
|
||||
///
|
||||
/// Defaults to:
|
||||
/// ```ignore
|
||||
#[doc = include_str!("window-app-manifest.xml")]
|
||||
/// [manifest]: https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn app_manifest<S: AsRef<str>>(mut self, manifest: S) -> Self {
|
||||
self.app_manifest = Some(manifest.as_ref().to_string());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// The attributes used on the build.
|
||||
|
@ -179,8 +227,8 @@ impl Attributes {
|
|||
/// This is typically desirable when running inside a build script; see [`try_build`] for no panics.
|
||||
pub fn build() {
|
||||
if let Err(error) = try_build(Attributes::default()) {
|
||||
let error = format!("{:#}", error);
|
||||
println!("{}", error);
|
||||
let error = format!("{error:#}");
|
||||
println!("{error}");
|
||||
if error.starts_with("unknown field") {
|
||||
print!("found an unknown configuration field. This usually means that you are using a CLI version that is newer than `tauri-build` and is incompatible. ");
|
||||
println!(
|
||||
|
@ -336,24 +384,11 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
|
|||
if window_icon_path.exists() {
|
||||
let mut res = WindowsResource::new();
|
||||
|
||||
res.set_manifest(
|
||||
r#"
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
</assembly>
|
||||
"#,
|
||||
);
|
||||
if let Some(manifest) = attributes.windows_attributes.app_manifest {
|
||||
res.set_manifest(&manifest);
|
||||
} else {
|
||||
res.set_manifest(include_str!("window-app-manifest.xml"));
|
||||
}
|
||||
|
||||
if let Some(sdk_dir) = &attributes.windows_attributes.sdk_dir {
|
||||
if let Some(sdk_dir_str) = sdk_dir.to_str() {
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
</assembly>
|
|
@ -14,7 +14,7 @@ readme = "README.md"
|
|||
|
||||
[dependencies]
|
||||
sha2 = "0.10"
|
||||
base64 = "0.13"
|
||||
base64 = "0.20"
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
serde = { version = "1", features = [ "derive" ] }
|
||||
|
@ -26,9 +26,9 @@ brotli = { version = "3", optional = true, default-features = false, features =
|
|||
regex = { version = "1.7.0", optional = true }
|
||||
uuid = { version = "1", features = [ "v4" ] }
|
||||
semver = "1"
|
||||
ico = "0.2"
|
||||
ico = "0.3"
|
||||
png = "0.17"
|
||||
json-patch = "0.2"
|
||||
json-patch = "0.3"
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
plist = "1"
|
||||
|
|
|
@ -141,7 +141,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
|
|||
} else if target.contains("apple-ios") {
|
||||
Target::Ios
|
||||
} else {
|
||||
panic!("unknown codegen target {}", target);
|
||||
panic!("unknown codegen target {target}");
|
||||
}
|
||||
} else if cfg!(target_os = "linux") {
|
||||
Target::Linux
|
||||
|
@ -370,10 +370,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
|
|||
PatternKind::Isolation { dir } => {
|
||||
let dir = config_parent.join(dir);
|
||||
if !dir.exists() {
|
||||
panic!(
|
||||
"The isolation application path is set to `{:?}` but it does not exist",
|
||||
dir
|
||||
)
|
||||
panic!("The isolation application path is set to `{dir:?}` but it does not exist")
|
||||
}
|
||||
|
||||
let mut sets_isolation_hook = false;
|
||||
|
@ -414,7 +411,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
|
|||
let shell_scope_open = match &config.tauri.allowlist.shell.open {
|
||||
ShellAllowlistOpen::Flag(false) => quote!(::std::option::Option::None),
|
||||
ShellAllowlistOpen::Flag(true) => {
|
||||
quote!(::std::option::Option::Some(#root::regex::Regex::new("^https?://").unwrap()))
|
||||
quote!(::std::option::Option::Some(#root::regex::Regex::new(r#"^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+"#).unwrap()))
|
||||
}
|
||||
ShellAllowlistOpen::Validate(regex) => match Regex::new(regex) {
|
||||
Ok(_) => quote!(::std::option::Option::Some(#root::regex::Regex::new(#regex).unwrap())),
|
||||
|
|
|
@ -343,14 +343,14 @@ impl EmbeddedAssets {
|
|||
|
||||
let mut hex = String::with_capacity(2 * bytes.len());
|
||||
for b in bytes {
|
||||
write!(hex, "{:02x}", b).map_err(EmbeddedAssetsError::Hex)?;
|
||||
write!(hex, "{b:02x}").map_err(EmbeddedAssetsError::Hex)?;
|
||||
}
|
||||
hex
|
||||
};
|
||||
|
||||
// use the content hash to determine filename, keep extensions that exist
|
||||
let out_path = if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
||||
out_dir.join(format!("{}.{}", hash, ext))
|
||||
out_dir.join(format!("{hash}.{ext}"))
|
||||
} else {
|
||||
out_dir.join(hash)
|
||||
};
|
||||
|
|
|
@ -13,7 +13,7 @@ exclude = [ "CHANGELOG.md", "/target" ]
|
|||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
wry = { version = "0.23", default-features = false, features = [ "file-drop", "protocol" ] }
|
||||
wry = { git = "https://github.com/tauri-apps/wry", default-features = false, features = [ "file-drop", "protocol" ] }
|
||||
tauri-runtime = { version = "0.12.1", path = "../tauri-runtime" }
|
||||
tauri-utils = { version = "1.2.1", path = "../tauri-utils" }
|
||||
uuid = { version = "1", features = [ "v4" ] }
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
// `alias` must be a snake case string.
|
||||
fn alias(alias: &str, has_feature: bool) {
|
||||
if has_feature {
|
||||
println!("cargo:rustc-cfg={}", alias);
|
||||
println!("cargo:rustc-cfg={alias}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,8 +14,8 @@ use tauri_runtime::{
|
|||
dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size},
|
||||
CursorIcon, DetachedWindow, FileDropEvent, JsEventListenerKey, PendingWindow, WindowEvent,
|
||||
},
|
||||
Dispatch, Error, EventLoopProxy, ExitRequestedEventAction, Icon, Result, RunEvent, RunIteration,
|
||||
Runtime, RuntimeHandle, UserAttentionType, UserEvent,
|
||||
DeviceEventFilter, Dispatch, Error, EventLoopProxy, ExitRequestedEventAction, Icon, Result,
|
||||
RunEvent, RunIteration, Runtime, RuntimeHandle, UserAttentionType, UserEvent,
|
||||
};
|
||||
|
||||
use tauri_runtime::window::MenuEvent;
|
||||
|
@ -33,6 +33,8 @@ use wry::application::platform::macos::WindowBuilderExtMacOS;
|
|||
use wry::application::platform::unix::{WindowBuilderExtUnix, WindowExtUnix};
|
||||
#[cfg(windows)]
|
||||
use wry::application::platform::windows::{WindowBuilderExtWindows, WindowExtWindows};
|
||||
#[cfg(windows)]
|
||||
use wry::webview::WebViewBuilderExtWindows;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri_utils::TitleBarStyle;
|
||||
|
@ -47,7 +49,8 @@ use wry::{
|
|||
},
|
||||
event::{Event, StartCause, WindowEvent as WryWindowEvent},
|
||||
event_loop::{
|
||||
ControlFlow, EventLoop, EventLoopProxy as WryEventLoopProxy, EventLoopWindowTarget,
|
||||
ControlFlow, DeviceEventFilter as WryDeviceEventFilter, EventLoop,
|
||||
EventLoopProxy as WryEventLoopProxy, EventLoopWindowTarget,
|
||||
},
|
||||
menu::{
|
||||
AboutMetadata as WryAboutMetadata, CustomMenuItem as WryCustomMenuItem, MenuBar,
|
||||
|
@ -61,10 +64,9 @@ use wry::{
|
|||
},
|
||||
},
|
||||
http::{Request as WryRequest, Response as WryResponse},
|
||||
webview::{FileDropEvent as WryFileDropEvent, WebContext, WebView, WebViewBuilder},
|
||||
webview::{FileDropEvent as WryFileDropEvent, Url, WebContext, WebView, WebViewBuilder},
|
||||
};
|
||||
|
||||
pub use wry;
|
||||
pub use wry::application::window::{Window, WindowBuilder as WryWindowBuilder, WindowId};
|
||||
|
||||
#[cfg(windows)]
|
||||
|
@ -79,6 +81,7 @@ pub use wry::application::platform::macos::{
|
|||
};
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
collections::{
|
||||
hash_map::Entry::{Occupied, Vacant},
|
||||
|
@ -284,7 +287,7 @@ impl From<&WryRequest<Vec<u8>>> for HttpRequestWrapper {
|
|||
}
|
||||
|
||||
// response
|
||||
struct HttpResponseWrapper(WryResponse<Vec<u8>>);
|
||||
struct HttpResponseWrapper(WryResponse<Cow<'static, [u8]>>);
|
||||
impl From<HttpResponse> for HttpResponseWrapper {
|
||||
fn from(response: HttpResponse) -> Self {
|
||||
let (parts, body) = response.into_parts();
|
||||
|
@ -298,7 +301,7 @@ impl From<HttpResponse> for HttpResponseWrapper {
|
|||
res_builder = res_builder.header(name, val);
|
||||
}
|
||||
|
||||
let res = res_builder.body(body).unwrap();
|
||||
let res = res_builder.body(Cow::Owned(body)).unwrap();
|
||||
Self(res)
|
||||
}
|
||||
}
|
||||
|
@ -364,6 +367,18 @@ impl From<MenuItem> for MenuItemWrapper {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct DeviceEventFilterWrapper(pub WryDeviceEventFilter);
|
||||
|
||||
impl From<DeviceEventFilter> for DeviceEventFilterWrapper {
|
||||
fn from(item: DeviceEventFilter) -> Self {
|
||||
match item {
|
||||
DeviceEventFilter::Always => Self(WryDeviceEventFilter::Always),
|
||||
DeviceEventFilter::Never => Self(WryDeviceEventFilter::Never),
|
||||
DeviceEventFilter::Unfocused => Self(WryDeviceEventFilter::Unfocused),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub struct NativeImageWrapper(pub WryNativeImage);
|
||||
|
||||
|
@ -705,6 +720,7 @@ impl WindowBuilder for WindowBuilderWrapper {
|
|||
.decorations(config.decorations)
|
||||
.maximized(config.maximized)
|
||||
.always_on_top(config.always_on_top)
|
||||
.content_protected(config.content_protected)
|
||||
.skip_taskbar(config.skip_taskbar)
|
||||
.theme(config.theme);
|
||||
|
||||
|
@ -839,6 +855,11 @@ impl WindowBuilder for WindowBuilderWrapper {
|
|||
self
|
||||
}
|
||||
|
||||
fn content_protected(mut self, protected: bool) -> Self {
|
||||
self.inner = self.inner.with_content_protection(protected);
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn parent_window(mut self, parent: HWND) -> Self {
|
||||
self.inner = self.inner.with_parent_window(parent);
|
||||
|
@ -1017,16 +1038,19 @@ pub enum WindowMessage {
|
|||
#[cfg(any(debug_assertions, feature = "devtools"))]
|
||||
IsDevToolsOpen(Sender<bool>),
|
||||
// Getters
|
||||
Url(Sender<Url>),
|
||||
ScaleFactor(Sender<f64>),
|
||||
InnerPosition(Sender<Result<PhysicalPosition<i32>>>),
|
||||
OuterPosition(Sender<Result<PhysicalPosition<i32>>>),
|
||||
InnerSize(Sender<PhysicalSize<u32>>),
|
||||
OuterSize(Sender<PhysicalSize<u32>>),
|
||||
IsFullscreen(Sender<bool>),
|
||||
IsMinimized(Sender<bool>),
|
||||
IsMaximized(Sender<bool>),
|
||||
IsDecorated(Sender<bool>),
|
||||
IsResizable(Sender<bool>),
|
||||
IsVisible(Sender<bool>),
|
||||
Title(Sender<String>),
|
||||
IsMenuVisible(Sender<bool>),
|
||||
CurrentMonitor(Sender<Option<MonitorHandle>>),
|
||||
PrimaryMonitor(Sender<Option<MonitorHandle>>),
|
||||
|
@ -1057,6 +1081,7 @@ pub enum WindowMessage {
|
|||
Close,
|
||||
SetDecorations(bool),
|
||||
SetAlwaysOnTop(bool),
|
||||
SetContentProtected(bool),
|
||||
SetSize(Size),
|
||||
SetMinSize(Option<Size>),
|
||||
SetMaxSize(Option<Size>),
|
||||
|
@ -1099,6 +1124,7 @@ pub enum TrayMessage {
|
|||
UpdateIconAsTemplate(bool),
|
||||
#[cfg(target_os = "macos")]
|
||||
UpdateTitle(String),
|
||||
UpdateTooltip(String),
|
||||
Create(SystemTray, Sender<Result<()>>),
|
||||
Destroy(Sender<Result<()>>),
|
||||
}
|
||||
|
@ -1215,6 +1241,10 @@ impl<T: UserEvent> Dispatch<T> for WryDispatcher<T> {
|
|||
|
||||
// Getters
|
||||
|
||||
fn url(&self) -> Result<Url> {
|
||||
window_getter!(self, WindowMessage::Url)
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> Result<f64> {
|
||||
window_getter!(self, WindowMessage::ScaleFactor)
|
||||
}
|
||||
|
@ -1239,6 +1269,10 @@ impl<T: UserEvent> Dispatch<T> for WryDispatcher<T> {
|
|||
window_getter!(self, WindowMessage::IsFullscreen)
|
||||
}
|
||||
|
||||
fn is_minimized(&self) -> Result<bool> {
|
||||
window_getter!(self, WindowMessage::IsMinimized)
|
||||
}
|
||||
|
||||
fn is_maximized(&self) -> Result<bool> {
|
||||
window_getter!(self, WindowMessage::IsMaximized)
|
||||
}
|
||||
|
@ -1257,6 +1291,10 @@ impl<T: UserEvent> Dispatch<T> for WryDispatcher<T> {
|
|||
window_getter!(self, WindowMessage::IsVisible)
|
||||
}
|
||||
|
||||
fn title(&self) -> Result<String> {
|
||||
window_getter!(self, WindowMessage::Title)
|
||||
}
|
||||
|
||||
fn is_menu_visible(&self) -> Result<bool> {
|
||||
window_getter!(self, WindowMessage::IsMenuVisible)
|
||||
}
|
||||
|
@ -1426,6 +1464,16 @@ impl<T: UserEvent> Dispatch<T> for WryDispatcher<T> {
|
|||
)
|
||||
}
|
||||
|
||||
fn set_content_protected(&self, protected: bool) -> Result<()> {
|
||||
send_user_message(
|
||||
&self.context,
|
||||
Message::Window(
|
||||
self.window_id,
|
||||
WindowMessage::SetContentProtected(protected),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn set_size(&self, size: Size) -> Result<()> {
|
||||
send_user_message(
|
||||
&self.context,
|
||||
|
@ -2028,6 +2076,12 @@ impl<T: UserEvent> Runtime<T> for Wry<T> {
|
|||
self.event_loop.hide_application();
|
||||
}
|
||||
|
||||
fn set_device_event_filter(&mut self, filter: DeviceEventFilter) {
|
||||
self
|
||||
.event_loop
|
||||
.set_device_event_filter(DeviceEventFilterWrapper::from(filter).0);
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
fn run_iteration<F: FnMut(RunEvent<T>) + 'static>(&mut self, mut callback: F) -> RunIteration {
|
||||
use wry::application::platform::run_return::EventLoopExtRunReturn;
|
||||
|
@ -2315,6 +2369,11 @@ fn handle_user_message<T: UserEvent>(
|
|||
}
|
||||
}
|
||||
// Getters
|
||||
WindowMessage::Url(tx) => {
|
||||
if let WindowHandle::Webview { inner: w, .. } = &window {
|
||||
tx.send(w.url()).unwrap();
|
||||
}
|
||||
}
|
||||
WindowMessage::ScaleFactor(tx) => tx.send(window.scale_factor()).unwrap(),
|
||||
WindowMessage::InnerPosition(tx) => tx
|
||||
.send(
|
||||
|
@ -2339,10 +2398,12 @@ fn handle_user_message<T: UserEvent>(
|
|||
.send(PhysicalSizeWrapper(window.outer_size()).into())
|
||||
.unwrap(),
|
||||
WindowMessage::IsFullscreen(tx) => tx.send(window.fullscreen().is_some()).unwrap(),
|
||||
WindowMessage::IsMinimized(tx) => tx.send(window.is_minimized()).unwrap(),
|
||||
WindowMessage::IsMaximized(tx) => tx.send(window.is_maximized()).unwrap(),
|
||||
WindowMessage::IsDecorated(tx) => tx.send(window.is_decorated()).unwrap(),
|
||||
WindowMessage::IsResizable(tx) => tx.send(window.is_resizable()).unwrap(),
|
||||
WindowMessage::IsVisible(tx) => tx.send(window.is_visible()).unwrap(),
|
||||
WindowMessage::Title(tx) => tx.send(window.title()).unwrap(),
|
||||
WindowMessage::IsMenuVisible(tx) => tx.send(window.is_menu_visible()).unwrap(),
|
||||
WindowMessage::CurrentMonitor(tx) => tx.send(window.current_monitor()).unwrap(),
|
||||
WindowMessage::PrimaryMonitor(tx) => tx.send(window.primary_monitor()).unwrap(),
|
||||
|
@ -2387,6 +2448,9 @@ fn handle_user_message<T: UserEvent>(
|
|||
}
|
||||
WindowMessage::SetDecorations(decorations) => window.set_decorations(decorations),
|
||||
WindowMessage::SetAlwaysOnTop(always_on_top) => window.set_always_on_top(always_on_top),
|
||||
WindowMessage::SetContentProtected(protected) => {
|
||||
window.set_content_protection(protected)
|
||||
}
|
||||
WindowMessage::SetSize(size) => {
|
||||
window.set_inner_size(SizeWrapper::from(size).0);
|
||||
}
|
||||
|
@ -2573,6 +2637,11 @@ fn handle_user_message<T: UserEvent>(
|
|||
tray.set_title(&title);
|
||||
}
|
||||
}
|
||||
TrayMessage::UpdateTooltip(tooltip) => {
|
||||
if let Some(tray) = &mut *tray_context.tray.lock().unwrap() {
|
||||
tray.set_tooltip(&tooltip);
|
||||
}
|
||||
}
|
||||
TrayMessage::Create(_tray, _tx) => {
|
||||
// already handled
|
||||
}
|
||||
|
@ -2788,6 +2857,19 @@ fn handle_event_loop<T: UserEvent>(
|
|||
}
|
||||
|
||||
match event {
|
||||
#[cfg(windows)]
|
||||
WryWindowEvent::ThemeChanged(theme) => {
|
||||
if let Some(window) = windows.borrow().get(&window_id) {
|
||||
if let Some(WindowHandle::Webview { inner, .. }) = &window.inner {
|
||||
let theme = match theme {
|
||||
WryTheme::Dark => wry::webview::Theme::Dark,
|
||||
WryTheme::Light => wry::webview::Theme::Light,
|
||||
_ => wry::webview::Theme::Light,
|
||||
};
|
||||
inner.set_theme(theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
WryWindowEvent::CloseRequested => {
|
||||
on_close_requested(callback, window_id, windows.clone());
|
||||
}
|
||||
|
@ -2962,6 +3044,9 @@ fn create_webview<T: UserEvent>(
|
|||
.with_drag_and_drop(webview_attributes.file_drop_handler_enabled);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
let window_theme = window_builder.inner.window.preferred_theme;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if window_builder.tabbing_identifier.is_none()
|
||||
|
@ -2998,9 +3083,29 @@ fn create_webview<T: UserEvent>(
|
|||
webview_builder = webview_builder
|
||||
.with_file_drop_handler(create_file_drop_handler(window_event_listeners.clone()));
|
||||
}
|
||||
if let Some(navigation_handler) = pending.navigation_handler {
|
||||
webview_builder = webview_builder.with_navigation_handler(move |url| {
|
||||
Url::parse(&url).map(&navigation_handler).unwrap_or(true)
|
||||
});
|
||||
}
|
||||
if let Some(user_agent) = webview_attributes.user_agent {
|
||||
webview_builder = webview_builder.with_user_agent(&user_agent);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
if let Some(additional_browser_args) = webview_attributes.additional_browser_args {
|
||||
webview_builder = webview_builder.with_additional_browser_args(&additional_browser_args);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
if let Some(theme) = window_theme {
|
||||
webview_builder = webview_builder.with_theme(match theme {
|
||||
WryTheme::Dark => wry::webview::Theme::Dark,
|
||||
WryTheme::Light => wry::webview::Theme::Light,
|
||||
_ => wry::webview::Theme::Light,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(handler) = ipc_handler {
|
||||
webview_builder = webview_builder.with_ipc_handler(create_ipc_handler(
|
||||
context,
|
||||
|
|
|
@ -114,6 +114,10 @@ pub fn create_tray<T>(
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(tooltip) = system_tray.tooltip {
|
||||
builder = builder.with_tooltip(&tooltip);
|
||||
}
|
||||
|
||||
let tray = builder
|
||||
.build(event_loop)
|
||||
.map_err(|e| Error::SystemTray(Box::new(e)))?;
|
||||
|
@ -172,6 +176,16 @@ impl<T: UserEvent> TrayHandle for SystemTrayHandle<T> {
|
|||
.map_err(|_| Error::FailedToSendMessage)
|
||||
}
|
||||
|
||||
fn set_tooltip(&self, tooltip: &str) -> Result<()> {
|
||||
self
|
||||
.proxy
|
||||
.send_event(Message::Tray(
|
||||
self.id,
|
||||
TrayMessage::UpdateTooltip(tooltip.to_owned()),
|
||||
))
|
||||
.map_err(|_| Error::FailedToSendMessage)
|
||||
}
|
||||
|
||||
fn destroy(&self) -> Result<()> {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
send_user_message(
|
||||
|
|
|
@ -32,6 +32,7 @@ http = "0.2.4"
|
|||
http-range = "0.1.4"
|
||||
raw-window-handle = "0.5"
|
||||
rand = "0.8"
|
||||
url = { version = "2" }
|
||||
|
||||
[target."cfg(windows)".dependencies]
|
||||
webview2-com = "0.19.1"
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
// `alias` must be a snake case string.
|
||||
fn alias(alias: &str, has_feature: bool) {
|
||||
if has_feature {
|
||||
println!("cargo:rustc-cfg={}", alias);
|
||||
println!("cargo:rustc-cfg={alias}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ use raw_window_handle::RawDisplayHandle;
|
|||
use serde::Deserialize;
|
||||
use std::{fmt::Debug, sync::mpsc::Sender};
|
||||
use tauri_utils::Theme;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod http;
|
||||
|
@ -53,6 +54,7 @@ pub struct SystemTray {
|
|||
#[cfg(target_os = "macos")]
|
||||
pub title: Option<String>,
|
||||
pub on_event: Option<Box<TrayEventHandler>>,
|
||||
pub tooltip: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(all(desktop, feature = "system-tray"))]
|
||||
|
@ -86,6 +88,7 @@ impl Clone for SystemTray {
|
|||
menu_on_left_click: self.menu_on_left_click,
|
||||
#[cfg(target_os = "macos")]
|
||||
title: self.title.clone(),
|
||||
tooltip: self.tooltip.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -104,6 +107,7 @@ impl Default for SystemTray {
|
|||
#[cfg(target_os = "macos")]
|
||||
title: None,
|
||||
on_event: None,
|
||||
tooltip: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -156,6 +160,17 @@ impl SystemTray {
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the tray icon tooltip.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Linux:** Unsupported
|
||||
#[must_use]
|
||||
pub fn with_tooltip(mut self, tooltip: &str) -> Self {
|
||||
self.tooltip = Some(tooltip.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the menu to show when the system tray is right clicked.
|
||||
#[must_use]
|
||||
pub fn with_menu(mut self, menu: menu::SystemTrayMenu) -> Self {
|
||||
|
@ -184,6 +199,23 @@ pub enum UserAttentionType {
|
|||
Informational,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum DeviceEventFilter {
|
||||
/// Always filter out device events.
|
||||
Always,
|
||||
/// Filter out device events while the window is not focused.
|
||||
Unfocused,
|
||||
/// Report all device events regardless of window focus.
|
||||
Never,
|
||||
}
|
||||
|
||||
impl Default for DeviceEventFilter {
|
||||
fn default() -> Self {
|
||||
Self::Unfocused
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
|
@ -461,6 +493,19 @@ pub trait Runtime<T: UserEvent>: Debug + Sized + 'static {
|
|||
#[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))]
|
||||
fn hide(&self);
|
||||
|
||||
/// Change the device event filter mode.
|
||||
///
|
||||
/// Since the DeviceEvent capture can lead to high CPU usage for unfocused windows, [`tao`]
|
||||
/// will ignore them by default for unfocused windows on Windows. This method allows changing
|
||||
/// the filter to explicitly capture them again.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - ** Linux / macOS / iOS / Android**: Unsupported.
|
||||
///
|
||||
/// [`tao`]: https://crates.io/crates/tao
|
||||
fn set_device_event_filter(&mut self, filter: DeviceEventFilter);
|
||||
|
||||
/// Runs the one step of the webview runtime event loop and returns control flow to the caller.
|
||||
#[cfg(desktop)]
|
||||
fn run_iteration<F: Fn(RunEvent<T>) + 'static>(&mut self, callback: F) -> RunIteration;
|
||||
|
@ -500,6 +545,9 @@ pub trait Dispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'static
|
|||
|
||||
// GETTERS
|
||||
|
||||
/// Returns the webview's current URL.
|
||||
fn url(&self) -> Result<Url>;
|
||||
|
||||
/// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa.
|
||||
fn scale_factor(&self) -> Result<f64>;
|
||||
|
||||
|
@ -522,6 +570,9 @@ pub trait Dispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'static
|
|||
/// Gets the window's current fullscreen state.
|
||||
fn is_fullscreen(&self) -> Result<bool>;
|
||||
|
||||
/// Gets the window's current minimized state.
|
||||
fn is_minimized(&self) -> Result<bool>;
|
||||
|
||||
/// Gets the window's current maximized state.
|
||||
fn is_maximized(&self) -> Result<bool>;
|
||||
|
||||
|
@ -533,6 +584,8 @@ pub trait Dispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'static
|
|||
|
||||
/// Gets the window's current visibility state.
|
||||
fn is_visible(&self) -> Result<bool>;
|
||||
/// Gets the window's current title.
|
||||
fn title(&self) -> Result<String>;
|
||||
|
||||
/// Gets the window menu current visibility state.
|
||||
fn is_menu_visible(&self) -> Result<bool>;
|
||||
|
@ -623,6 +676,9 @@ pub trait Dispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'static
|
|||
/// Updates the window alwaysOnTop flag.
|
||||
fn set_always_on_top(&self, always_on_top: bool) -> Result<()>;
|
||||
|
||||
/// Prevents the window contents from being captured by other apps.
|
||||
fn set_content_protected(&self, protected: bool) -> Result<()>;
|
||||
|
||||
/// Resizes the window.
|
||||
fn set_size(&self, size: Size) -> Result<()>;
|
||||
|
||||
|
|
|
@ -154,6 +154,7 @@ pub trait TrayHandle: fmt::Debug + Clone + Send + Sync {
|
|||
fn set_icon_as_template(&self, is_template: bool) -> crate::Result<()>;
|
||||
#[cfg(target_os = "macos")]
|
||||
fn set_title(&self, title: &str) -> crate::Result<()>;
|
||||
fn set_tooltip(&self, tooltip: &str) -> crate::Result<()>;
|
||||
fn destroy(&self) -> crate::Result<()>;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ pub struct WebviewAttributes {
|
|||
pub file_drop_handler_enabled: bool,
|
||||
pub clipboard: bool,
|
||||
pub accept_first_mouse: bool,
|
||||
pub additional_browser_args: Option<String>,
|
||||
}
|
||||
|
||||
impl WebviewAttributes {
|
||||
|
@ -41,6 +42,7 @@ impl WebviewAttributes {
|
|||
file_drop_handler_enabled: true,
|
||||
clipboard: false,
|
||||
accept_first_mouse: false,
|
||||
additional_browser_args: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,6 +90,13 @@ impl WebviewAttributes {
|
|||
self.accept_first_mouse = accept;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets additional browser arguments. **Windows Only**
|
||||
#[must_use]
|
||||
pub fn additional_browser_args(mut self, additional_args: &str) -> Self {
|
||||
self.additional_browser_args = Some(additional_args.to_string());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Do **NOT** implement this trait except for use in a custom [`Runtime`](crate::Runtime).
|
||||
|
@ -172,6 +181,10 @@ pub trait WindowBuilder: WindowBuilderBase {
|
|||
#[must_use]
|
||||
fn always_on_top(self, always_on_top: bool) -> Self;
|
||||
|
||||
/// Prevents the window contents from being captured by other apps.
|
||||
#[must_use]
|
||||
fn content_protected(self, protected: bool) -> Self;
|
||||
|
||||
/// Sets the window icon.
|
||||
fn icon(self, icon: Icon) -> crate::Result<Self>;
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ use crate::{
|
|||
};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use tauri_utils::{config::WindowConfig, Theme};
|
||||
use url::Url;
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
|
@ -232,6 +233,9 @@ pub struct PendingWindow<T: UserEvent, R: Runtime<T>> {
|
|||
|
||||
/// A HashMap mapping JS event names with associated listener ids.
|
||||
pub js_event_listeners: Arc<Mutex<HashMap<JsEventListenerKey, HashSet<u64>>>>,
|
||||
|
||||
/// A handler to decide if incoming url is allowed to navigate.
|
||||
pub navigation_handler: Option<Box<dyn Fn(Url) -> bool + Send>>,
|
||||
}
|
||||
|
||||
pub fn is_label_valid(label: &str) -> bool {
|
||||
|
@ -271,6 +275,7 @@ impl<T: UserEvent, R: Runtime<T>> PendingWindow<T, R> {
|
|||
url: "tauri://localhost".to_string(),
|
||||
menu_ids: Arc::new(Mutex::new(menu_ids)),
|
||||
js_event_listeners: Default::default(),
|
||||
navigation_handler: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -300,6 +305,7 @@ impl<T: UserEvent, R: Runtime<T>> PendingWindow<T, R> {
|
|||
url: "tauri://localhost".to_string(),
|
||||
menu_ids: Arc::new(Mutex::new(menu_ids)),
|
||||
js_event_listeners: Default::default(),
|
||||
navigation_handler: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ pub enum WindowUrl {
|
|||
impl fmt::Display for WindowUrl {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::External(url) => write!(f, "{}", url),
|
||||
Self::External(url) => write!(f, "{url}"),
|
||||
Self::App(path) => write!(f, "{}", path.display()),
|
||||
}
|
||||
}
|
||||
|
@ -78,6 +78,8 @@ pub enum BundleType {
|
|||
AppImage,
|
||||
/// The Microsoft Installer bundle (.msi).
|
||||
Msi,
|
||||
/// The NSIS bundle (.exe).
|
||||
Nsis,
|
||||
/// The macOS application bundle (.app).
|
||||
App,
|
||||
/// The Apple Disk Image bundle (.dmg).
|
||||
|
@ -95,6 +97,7 @@ impl Display for BundleType {
|
|||
Self::Deb => "deb",
|
||||
Self::AppImage => "appimage",
|
||||
Self::Msi => "msi",
|
||||
Self::Nsis => "nsis",
|
||||
Self::App => "app",
|
||||
Self::Dmg => "dmg",
|
||||
Self::Updater => "updater",
|
||||
|
@ -122,10 +125,11 @@ impl<'de> Deserialize<'de> for BundleType {
|
|||
"deb" => Ok(Self::Deb),
|
||||
"appimage" => Ok(Self::AppImage),
|
||||
"msi" => Ok(Self::Msi),
|
||||
"nsis" => Ok(Self::Nsis),
|
||||
"app" => Ok(Self::App),
|
||||
"dmg" => Ok(Self::Dmg),
|
||||
"updater" => Ok(Self::Updater),
|
||||
_ => Err(DeError::custom(format!("unknown bundle target '{}'", s))),
|
||||
_ => Err(DeError::custom(format!("unknown bundle target '{s}'"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -223,7 +227,7 @@ impl<'de> Deserialize<'de> for BundleTarget {
|
|||
|
||||
match BundleTargetInner::deserialize(deserializer)? {
|
||||
BundleTargetInner::All(s) if s.to_lowercase() == "all" => Ok(Self::All),
|
||||
BundleTargetInner::All(t) => Err(DeError::custom(format!("invalid bundle type {}", t))),
|
||||
BundleTargetInner::All(t) => Err(DeError::custom(format!("invalid bundle type {t}"))),
|
||||
BundleTargetInner::List(l) => Ok(Self::List(l)),
|
||||
BundleTargetInner::One(t) => Ok(Self::One(t)),
|
||||
}
|
||||
|
@ -416,6 +420,58 @@ pub struct WixConfig {
|
|||
pub dialog_image_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Configuration for the Installer bundle using NSIS.
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(JsonSchema))]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct NsisConfig {
|
||||
/// The path to the license file to render on the installer.
|
||||
pub license: Option<PathBuf>,
|
||||
/// The path to a bitmap file to display on the header of installers pages.
|
||||
///
|
||||
/// The recommended dimensions are 150px x 57px.
|
||||
pub header_image: Option<PathBuf>,
|
||||
/// The path to a bitmap file for the Welcome page and the Finish page.
|
||||
///
|
||||
/// The recommended dimensions are 164px x 314px.
|
||||
pub sidebar_image: Option<PathBuf>,
|
||||
/// The path to an icon file used as the installer icon.
|
||||
pub installer_icon: Option<PathBuf>,
|
||||
/// Whether the installation will be for all users or just the current user.
|
||||
#[serde(default)]
|
||||
pub install_mode: NSISInstallerMode,
|
||||
}
|
||||
|
||||
/// Install Modes for the NSIS installer.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[cfg_attr(feature = "schema", derive(JsonSchema))]
|
||||
pub enum NSISInstallerMode {
|
||||
/// Default mode for the installer.
|
||||
///
|
||||
/// Install the app by default in a directory that doesn't require Administrator access.
|
||||
///
|
||||
/// Installer metadata will be saved under the `HKCU` registry path.
|
||||
CurrentUser,
|
||||
/// Install the app by default in the `Program Files` folder directory requires Administrator
|
||||
/// access for the installation.
|
||||
///
|
||||
/// Installer metadata will be saved under the `HKLM` registry path.
|
||||
PerMachine,
|
||||
/// Combines both modes and allows the user to choose at install time
|
||||
/// whether to install for the current user or per machine. Note that this mode
|
||||
/// will require Administrator access even if the user wants to install it for the current user only.
|
||||
///
|
||||
/// Installer metadata will be saved under the `HKLM` or `HKCU` registry path based on the user's choice.
|
||||
Both,
|
||||
}
|
||||
|
||||
impl Default for NSISInstallerMode {
|
||||
fn default() -> Self {
|
||||
Self::CurrentUser
|
||||
}
|
||||
}
|
||||
|
||||
/// Install modes for the Webview2 runtime.
|
||||
/// Note that for the updater bundle [`Self::DownloadBootstrapper`] is used.
|
||||
///
|
||||
|
@ -512,6 +568,8 @@ pub struct WindowsConfig {
|
|||
pub allow_downgrades: bool,
|
||||
/// Configuration for the MSI generated with WiX.
|
||||
pub wix: Option<WixConfig>,
|
||||
/// Configuration for the installer generated with NSIS.
|
||||
pub nsis: Option<NsisConfig>,
|
||||
}
|
||||
|
||||
impl Default for WindowsConfig {
|
||||
|
@ -525,6 +583,7 @@ impl Default for WindowsConfig {
|
|||
webview_fixed_runtime_path: None,
|
||||
allow_downgrades: default_allow_downgrades(),
|
||||
wix: None,
|
||||
nsis: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -542,7 +601,7 @@ pub struct BundleConfig {
|
|||
/// Whether Tauri should bundle your application or just output the executable.
|
||||
#[serde(default)]
|
||||
pub active: bool,
|
||||
/// The bundle targets, currently supports ["deb", "appimage", "msi", "app", "dmg", "updater"] or "all".
|
||||
/// The bundle targets, currently supports ["deb", "appimage", "nsis", "msi", "app", "dmg", "updater"] or "all".
|
||||
#[serde(default)]
|
||||
pub targets: BundleTarget,
|
||||
/// The application identifier in reverse domain name notation (e.g. `com.tauri.example`).
|
||||
|
@ -859,6 +918,9 @@ pub struct WindowConfig {
|
|||
/// Whether the window should always be on top of other windows.
|
||||
#[serde(default, alias = "always-on-top")]
|
||||
pub always_on_top: bool,
|
||||
/// Prevents the window contents from being captured by other apps.
|
||||
#[serde(default, alias = "content-protected")]
|
||||
pub content_protected: bool,
|
||||
/// If `true`, hides the window icon from the taskbar on Windows and Linux.
|
||||
#[serde(default, alias = "skip-taskbar")]
|
||||
pub skip_taskbar: bool,
|
||||
|
@ -881,6 +943,10 @@ pub struct WindowConfig {
|
|||
/// [tabbing identifier]: <https://developer.apple.com/documentation/appkit/nswindow/1644704-tabbingidentifier>
|
||||
#[serde(default, alias = "tabbing-identifier")]
|
||||
pub tabbing_identifier: Option<String>,
|
||||
/// Defines additional browser arguments on Windows. By default wry passes `--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection`
|
||||
/// so if you use this method, you also need to disable these components by yourself if you want.
|
||||
#[serde(default, alias = "additional-browser-args")]
|
||||
pub additional_browser_args: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for WindowConfig {
|
||||
|
@ -908,12 +974,14 @@ impl Default for WindowConfig {
|
|||
visible: default_visible(),
|
||||
decorations: default_decorations(),
|
||||
always_on_top: false,
|
||||
content_protected: false,
|
||||
skip_taskbar: false,
|
||||
theme: None,
|
||||
title_bar_style: Default::default(),
|
||||
hidden_title: false,
|
||||
accept_first_mouse: false,
|
||||
tabbing_identifier: None,
|
||||
additional_browser_args: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -985,7 +1053,7 @@ impl CspDirectiveSources {
|
|||
/// Whether the given source is configured on this directive or not.
|
||||
pub fn contains(&self, source: &str) -> bool {
|
||||
match self {
|
||||
Self::Inline(s) => s.contains(&format!("{} ", source)) || s.contains(&format!(" {}", source)),
|
||||
Self::Inline(s) => s.contains(&format!("{source} ")) || s.contains(&format!(" {source}")),
|
||||
Self::List(l) => l.contains(&source.into()),
|
||||
}
|
||||
}
|
||||
|
@ -1051,7 +1119,7 @@ impl From<Csp> for HashMap<String, CspDirectiveSources> {
|
|||
impl Display for Csp {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Policy(s) => write!(f, "{}", s),
|
||||
Self::Policy(s) => write!(f, "{s}"),
|
||||
Self::DirectiveMap(m) => {
|
||||
let len = m.len();
|
||||
let mut i = 0;
|
||||
|
@ -1329,6 +1397,9 @@ pub struct WindowAllowlistConfig {
|
|||
/// Allows setting the always_on_top flag of the window.
|
||||
#[serde(default, alias = "set-always-on-top")]
|
||||
pub set_always_on_top: bool,
|
||||
/// Allows preventing the window contents from being captured by other apps.
|
||||
#[serde(default, alias = "set-content-protected")]
|
||||
pub set_content_protected: bool,
|
||||
/// Allows setting the window size.
|
||||
#[serde(default, alias = "set-size")]
|
||||
pub set_size: bool,
|
||||
|
@ -1394,6 +1465,7 @@ impl Allowlist for WindowAllowlistConfig {
|
|||
close: true,
|
||||
set_decorations: true,
|
||||
set_always_on_top: true,
|
||||
set_content_protected: false,
|
||||
set_size: true,
|
||||
set_min_size: true,
|
||||
set_max_size: true,
|
||||
|
@ -1444,6 +1516,12 @@ impl Allowlist for WindowAllowlistConfig {
|
|||
set_always_on_top,
|
||||
"window-set-always-on-top"
|
||||
);
|
||||
check_feature!(
|
||||
self,
|
||||
features,
|
||||
set_content_protected,
|
||||
"window-set-content-protected"
|
||||
);
|
||||
check_feature!(self, features, set_size, "window-set-size");
|
||||
check_feature!(self, features, set_min_size, "window-set-min-size");
|
||||
check_feature!(self, features, set_max_size, "window-set-max-size");
|
||||
|
@ -1599,7 +1677,7 @@ pub struct ShellAllowlistScope(pub Vec<ShellAllowedCommand>);
|
|||
pub enum ShellAllowlistOpen {
|
||||
/// If the shell open API should be enabled.
|
||||
///
|
||||
/// If enabled, the default validation regex (`^https?://`) is used.
|
||||
/// If enabled, the default validation regex (`^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`) is used.
|
||||
Flag(bool),
|
||||
|
||||
/// Enable the shell open API, with a custom regex that the opened path must match against.
|
||||
|
@ -2062,7 +2140,16 @@ impl Allowlist for AppAllowlistConfig {
|
|||
}
|
||||
}
|
||||
|
||||
/// Allowlist configuration.
|
||||
/// Allowlist configuration. The allowlist is a translation of the [Cargo allowlist features](https://docs.rs/tauri/latest/tauri/#cargo-allowlist-features).
|
||||
///
|
||||
/// # Notes
|
||||
///
|
||||
/// - Endpoints that don't have their own allowlist option are enabled by default.
|
||||
/// - There is only "opt-in", no "opt-out". Setting an option to `false` has no effect.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// - * [`"app-all": true`](https://tauri.app/v1/api/config/#appallowlistconfig.all) will make the [hide](https://tauri.app/v1/api/js/app#hide) endpoint be available regardless of whether `hide` is set to `false` or `true` in the allowlist.
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(JsonSchema))]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
|
@ -2287,7 +2374,7 @@ pub enum WindowsUpdateInstallMode {
|
|||
/// Specifies there's a basic UI during the installation process, including a final dialog box at the end.
|
||||
BasicUi,
|
||||
/// The quiet mode means there's no user interaction required.
|
||||
/// Requires admin privileges if the installer does.
|
||||
/// Requires admin privileges if the installer does (WiX).
|
||||
Quiet,
|
||||
/// Specifies unattended mode, which means the installation only shows a progress bar.
|
||||
Passive,
|
||||
|
@ -2346,8 +2433,7 @@ impl<'de> Deserialize<'de> for WindowsUpdateInstallMode {
|
|||
"quiet" => Ok(Self::Quiet),
|
||||
"passive" => Ok(Self::Passive),
|
||||
_ => Err(DeError::custom(format!(
|
||||
"unknown update install mode '{}'",
|
||||
s
|
||||
"unknown update install mode '{s}'"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
@ -2359,6 +2445,9 @@ impl<'de> Deserialize<'de> for WindowsUpdateInstallMode {
|
|||
#[cfg_attr(feature = "schema", derive(JsonSchema))]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct UpdaterWindowsConfig {
|
||||
/// Additional arguments given to the NSIS or WiX installer.
|
||||
#[serde(default, alias = "installer-args")]
|
||||
pub installer_args: Vec<String>,
|
||||
/// The installation mode for the update on Windows. Defaults to `passive`.
|
||||
#[serde(default, alias = "install-mode")]
|
||||
pub install_mode: WindowsUpdateInstallMode,
|
||||
|
@ -2491,7 +2580,7 @@ pub enum AppUrl {
|
|||
impl std::fmt::Display for AppUrl {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Url(url) => write!(f, "{}", url),
|
||||
Self::Url(url) => write!(f, "{url}"),
|
||||
Self::Files(files) => write!(f, "{}", serde_json::to_string(files).unwrap()),
|
||||
}
|
||||
}
|
||||
|
@ -2630,9 +2719,9 @@ impl<'d> serde::Deserialize<'d> for PackageVersion {
|
|||
let path = PathBuf::from(value);
|
||||
if path.exists() {
|
||||
let json_str = read_to_string(&path)
|
||||
.map_err(|e| DeError::custom(format!("failed to read version JSON file: {}", e)))?;
|
||||
.map_err(|e| DeError::custom(format!("failed to read version JSON file: {e}")))?;
|
||||
let package_json: serde_json::Value = serde_json::from_str(&json_str)
|
||||
.map_err(|e| DeError::custom(format!("failed to read version JSON file: {}", e)))?;
|
||||
.map_err(|e| DeError::custom(format!("failed to read version JSON file: {e}")))?;
|
||||
if let Some(obj) = package_json.as_object() {
|
||||
let version = obj
|
||||
.get("version")
|
||||
|
@ -3040,12 +3129,14 @@ mod build {
|
|||
let visible = self.visible;
|
||||
let decorations = self.decorations;
|
||||
let always_on_top = self.always_on_top;
|
||||
let content_protected = self.content_protected;
|
||||
let skip_taskbar = self.skip_taskbar;
|
||||
let theme = opt_lit(self.theme.as_ref());
|
||||
let title_bar_style = &self.title_bar_style;
|
||||
let hidden_title = self.hidden_title;
|
||||
let accept_first_mouse = self.accept_first_mouse;
|
||||
let tabbing_identifier = opt_str_lit(self.tabbing_identifier.as_ref());
|
||||
let additional_browser_args = opt_str_lit(self.additional_browser_args.as_ref());
|
||||
|
||||
literal_struct!(
|
||||
tokens,
|
||||
|
@ -3072,12 +3163,14 @@ mod build {
|
|||
visible,
|
||||
decorations,
|
||||
always_on_top,
|
||||
content_protected,
|
||||
skip_taskbar,
|
||||
theme,
|
||||
title_bar_style,
|
||||
hidden_title,
|
||||
accept_first_mouse,
|
||||
tabbing_identifier
|
||||
tabbing_identifier,
|
||||
additional_browser_args
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -3333,7 +3426,8 @@ mod build {
|
|||
impl ToTokens for UpdaterWindowsConfig {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let install_mode = &self.install_mode;
|
||||
literal_struct!(tokens, UpdaterWindowsConfig, install_mode);
|
||||
let installer_args = vec_lit(&self.installer_args, str_lit);
|
||||
literal_struct!(tokens, UpdaterWindowsConfig, install_mode, installer_args);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ fn serialize_node_ref_internal<S: Serializer>(
|
|||
traversal_scope: TraversalScope,
|
||||
) -> crate::Result<()> {
|
||||
match (traversal_scope, node.data()) {
|
||||
(ref scope, &NodeData::Element(ref element)) => {
|
||||
(ref scope, NodeData::Element(element)) => {
|
||||
if *scope == TraversalScope::IncludeNode {
|
||||
let attrs = element.attributes.borrow();
|
||||
|
||||
|
@ -82,16 +82,16 @@ fn serialize_node_ref_internal<S: Serializer>(
|
|||
|
||||
(TraversalScope::ChildrenOnly(_), _) => Ok(()),
|
||||
|
||||
(TraversalScope::IncludeNode, &NodeData::Doctype(ref doctype)) => {
|
||||
(TraversalScope::IncludeNode, NodeData::Doctype(doctype)) => {
|
||||
serializer.write_doctype(&doctype.name).map_err(Into::into)
|
||||
}
|
||||
(TraversalScope::IncludeNode, &NodeData::Text(ref text)) => {
|
||||
(TraversalScope::IncludeNode, NodeData::Text(text)) => {
|
||||
serializer.write_text(&text.borrow()).map_err(Into::into)
|
||||
}
|
||||
(TraversalScope::IncludeNode, &NodeData::Comment(ref text)) => {
|
||||
(TraversalScope::IncludeNode, NodeData::Comment(text)) => {
|
||||
serializer.write_comment(&text.borrow()).map_err(Into::into)
|
||||
}
|
||||
(TraversalScope::IncludeNode, &NodeData::ProcessingInstruction(ref contents)) => {
|
||||
(TraversalScope::IncludeNode, NodeData::ProcessingInstruction(contents)) => {
|
||||
let contents = contents.borrow();
|
||||
serializer
|
||||
.write_processing_instruction(&contents.0, &contents.1)
|
||||
|
|
|
@ -39,7 +39,7 @@ impl std::fmt::Display for MimeType {
|
|||
MimeType::Svg => "image/svg+xml",
|
||||
MimeType::Mp4 => "video/mp4",
|
||||
};
|
||||
write!(f, "{}", mime)
|
||||
write!(f, "{mime}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -133,10 +133,10 @@ pub fn target_triple() -> crate::Result<String> {
|
|||
return Err(crate::Error::Environment);
|
||||
};
|
||||
|
||||
format!("{}-{}", os, env)
|
||||
format!("{os}-{env}")
|
||||
};
|
||||
|
||||
Ok(format!("{}-{}", arch, os))
|
||||
Ok(format!("{arch}-{os}"))
|
||||
}
|
||||
|
||||
/// Computes the resource directory of the current environment.
|
||||
|
@ -157,8 +157,8 @@ pub fn resource_dir(package_info: &PackageInfo, env: &Env) -> crate::Result<Path
|
|||
let exe_dir = exe.parent().expect("failed to get exe directory");
|
||||
let curr_dir = exe_dir.display().to_string();
|
||||
|
||||
if curr_dir.ends_with(format!("{S}target{S}debug", S = MAIN_SEPARATOR).as_str())
|
||||
|| curr_dir.ends_with(format!("{S}target{S}release", S = MAIN_SEPARATOR).as_str())
|
||||
if curr_dir.ends_with(format!("{MAIN_SEPARATOR}target{MAIN_SEPARATOR}debug").as_str())
|
||||
|| curr_dir.ends_with(format!("{MAIN_SEPARATOR}target{MAIN_SEPARATOR}release").as_str())
|
||||
|| cfg!(target_os = "windows")
|
||||
{
|
||||
// running from the out dir or windows
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
# Changelog
|
||||
|
||||
## \[1.2.3]
|
||||
|
||||
- Fix the filesystem scope allowing sub-directories of the directory picked by the dialog when `recursive` option was `false`.
|
||||
- [f1b0ad6e](https://www.github.com/tauri-apps/tauri/commit/f1b0ad6e8b721cf1420a9a4b9be5b05c39941d16) Merge pull request from GHSA-6mv3-wm7j-h4w5 on 2022-12-22
|
||||
|
||||
## \[1.2.2]
|
||||
|
||||
- Invoke event listener in windows safely to avoid causing uncaught errors in windows that have loaded external urls
|
||||
|
@ -67,6 +72,11 @@
|
|||
- Added the `user_agent` option when creating a window.
|
||||
- [a6c94119](https://www.github.com/tauri-apps/tauri/commit/a6c94119d8545d509723b147c273ca5edfe3729f) feat(core): expose user_agent to window config ([#5317](https://www.github.com/tauri-apps/tauri/pull/5317)) on 2022-10-02
|
||||
|
||||
## \[1.1.3]
|
||||
|
||||
- Fix the filesystem scope allowing sub-directories of the directory picked by the dialog when `recursive` option was `false`.
|
||||
- [2654c0f4](https://www.github.com/tauri-apps/tauri/commit/2654c0f49da23434d36447d0908fa24e61ff5e4e) Merge pull request from GHSA-6mv3-wm7j-h4w5 on 2022-12-22
|
||||
|
||||
## \[1.1.2]
|
||||
|
||||
- Escape glob special characters in files/directories when dropping files or using the open/save dialogs.
|
||||
|
@ -117,6 +127,11 @@
|
|||
- Add `exists` function to the fs module.
|
||||
- [3c62dbc9](https://www.github.com/tauri-apps/tauri/commit/3c62dbc902c904d35a7472ce72a969084c95fbbe) feat(api): Add `exists` function to the fs module. ([#5060](https://www.github.com/tauri-apps/tauri/pull/5060)) on 2022-09-15
|
||||
|
||||
## \[1.0.8]
|
||||
|
||||
- Fix the filesystem scope allowing sub-directories of the directory picked by the dialog when `recursive` option was `false`.
|
||||
- [f0602e7c](https://www.github.com/tauri-apps/tauri/commit/f0602e7c294245ab6ef6fbf2a976ef398340ef58) Merge pull request from GHSA-6mv3-wm7j-h4w5 on 2022-12-22
|
||||
|
||||
## \[1.0.7]
|
||||
|
||||
- Escape glob special characters in files/directories when dropping files or using the open/save dialogs.
|
||||
|
|
|
@ -10,7 +10,7 @@ license = "Apache-2.0 OR MIT"
|
|||
name = "tauri"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/tauri-apps/tauri"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
no-default-features = true
|
||||
|
@ -87,7 +87,7 @@ ico = { version = "0.2.0", optional = true }
|
|||
encoding_rs = "0.8.31"
|
||||
|
||||
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
|
||||
rfd = { version = "0.10", optional = true }
|
||||
rfd = { version = "0.10", optional = true, features=["gtk3", "common-controls-v6"] }
|
||||
notify-rust = { version = "4.5", default-features = false, features = [ "d" ], optional = true }
|
||||
|
||||
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
|
||||
|
@ -240,6 +240,7 @@ window-all = [
|
|||
"window-close",
|
||||
"window-set-decorations",
|
||||
"window-set-always-on-top",
|
||||
"window-set-content-protected",
|
||||
"window-set-size",
|
||||
"window-set-min-size",
|
||||
"window-set-max-size",
|
||||
|
@ -270,6 +271,7 @@ window-hide = [ ]
|
|||
window-close = [ ]
|
||||
window-set-decorations = [ ]
|
||||
window-set-always-on-top = [ ]
|
||||
window-set-content-protected = [ ]
|
||||
window-set-size = [ ]
|
||||
window-set-min-size = [ ]
|
||||
window-set-max-size = [ ]
|
||||
|
|
|
@ -31,7 +31,7 @@ fn has_feature(feature: &str) -> bool {
|
|||
// `alias` must be a snake case string.
|
||||
fn alias(alias: &str, has_feature: bool) {
|
||||
if has_feature {
|
||||
println!("cargo:rustc-cfg={}", alias);
|
||||
println!("cargo:rustc-cfg={alias}");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,6 +81,7 @@ fn main() {
|
|||
"close",
|
||||
"set-decorations",
|
||||
"set-always-on-top",
|
||||
"set-content-protected",
|
||||
"set-size",
|
||||
"set-min-size",
|
||||
"set-max-size",
|
||||
|
@ -140,6 +141,18 @@ fn main() {
|
|||
CHECKED_FEATURES.get().unwrap().lock().unwrap().join(","),
|
||||
)
|
||||
.expect("failed to write checked_features file");
|
||||
|
||||
// workaround needed to preven `STATUS_ENTRYPOINT_NOT_FOUND` error
|
||||
// see https://github.com/tauri-apps/tauri/pull/4383#issuecomment-1212221864
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS");
|
||||
let target_env = std::env::var("CARGO_CFG_TARGET_ENV");
|
||||
let is_tauri_workspace = std::env::var("__TAURI_WORKSPACE__").map_or(false, |v| v == "true");
|
||||
if is_tauri_workspace
|
||||
&& Ok("windows") == target_os.as_deref()
|
||||
&& Ok("msvc") == target_env.as_deref()
|
||||
{
|
||||
add_manifest();
|
||||
}
|
||||
}
|
||||
|
||||
// create aliases for the given module with its apis.
|
||||
|
@ -152,14 +165,14 @@ fn main() {
|
|||
//
|
||||
// Note that both `module` and `apis` strings must be written in kebab case.
|
||||
fn alias_module(module: &str, apis: &[&str], api_all: bool) {
|
||||
let all_feature_name = format!("{}-all", module);
|
||||
let all_feature_name = format!("{module}-all");
|
||||
let all = has_feature(&all_feature_name) || api_all;
|
||||
alias(&all_feature_name.to_snake_case(), all);
|
||||
|
||||
let mut any = all;
|
||||
|
||||
for api in apis {
|
||||
let has = has_feature(&format!("{}-{}", module, api)) || all;
|
||||
let has = has_feature(&format!("{module}-{api}")) || all;
|
||||
alias(
|
||||
&format!("{}_{}", AsSnakeCase(module), AsSnakeCase(api)),
|
||||
has,
|
||||
|
@ -169,3 +182,22 @@ fn alias_module(module: &str, apis: &[&str], api_all: bool) {
|
|||
|
||||
alias(&format!("{}_any", AsSnakeCase(module)), any);
|
||||
}
|
||||
|
||||
fn add_manifest() {
|
||||
static WINDOWS_MANIFEST_FILE: &str = "window-app-manifest.xml";
|
||||
|
||||
let manifest = std::env::current_dir()
|
||||
.unwrap()
|
||||
.join("../tauri-build/src")
|
||||
.join(WINDOWS_MANIFEST_FILE);
|
||||
|
||||
println!("cargo:rerun-if-changed={}", manifest.display());
|
||||
// Embed the Windows application manifest file.
|
||||
println!("cargo:rustc-link-arg=/MANIFEST:EMBED");
|
||||
println!(
|
||||
"cargo:rustc-link-arg=/MANIFESTINPUT:{}",
|
||||
manifest.to_str().unwrap()
|
||||
);
|
||||
// Turn linker warnings into errors.
|
||||
println!("cargo:rustc-link-arg=/WX");
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
;(function () {
|
||||
; (function () {
|
||||
function uid() {
|
||||
return window.crypto.getRandomValues(new Uint32Array(1))[0]
|
||||
}
|
||||
|
@ -96,7 +96,7 @@
|
|||
if (target.matches('a')) {
|
||||
if (
|
||||
target.href &&
|
||||
target.href.startsWith('http') &&
|
||||
(['http://', 'https://', 'mailto:', 'tel:'].some(v => target.href.startsWith(v))) &&
|
||||
target.target === '_blank'
|
||||
) {
|
||||
window.__TAURI_INVOKE__('tauri', {
|
||||
|
@ -136,6 +136,10 @@
|
|||
if (e.target.hasAttribute('data-tauri-drag-region') && e.buttons === 1) {
|
||||
// prevents text cursor
|
||||
e.preventDefault()
|
||||
// fix #2549: double click on drag region edge causes content to maximize without window sizing change
|
||||
// https://github.com/tauri-apps/tauri/issues/2549#issuecomment-1250036908
|
||||
e.stopImmediatePropagation()
|
||||
|
||||
// start dragging if the element has a `tauri-drag-region` data attribute and maximize on double-clicking it
|
||||
window.__TAURI_INVOKE__('tauri', {
|
||||
__tauriModule: 'Window',
|
||||
|
@ -151,15 +155,6 @@
|
|||
}
|
||||
})
|
||||
|
||||
listen('tauri://window-created', function (event) {
|
||||
if (event.payload) {
|
||||
var windowLabel = event.payload.label
|
||||
window.__TAURI_METADATA__.__windows.push({
|
||||
label: windowLabel
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
let permissionSettable = false
|
||||
let permissionValue = 'default'
|
||||
|
||||
|
|
|
@ -171,7 +171,7 @@ macro_rules! message_dialog_builder {
|
|||
|
||||
/// Options for action buttons on message dialogs.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum MessageDialogButtons {
|
||||
/// Ok button.
|
||||
Ok,
|
||||
|
@ -179,6 +179,10 @@ pub enum MessageDialogButtons {
|
|||
OkCancel,
|
||||
/// Yes and No buttons.
|
||||
YesNo,
|
||||
/// OK button with customized text.
|
||||
OkWithLabel(String),
|
||||
/// Ok and Cancel buttons with customized text.
|
||||
OkCancelWithLabels(String, String),
|
||||
}
|
||||
|
||||
impl From<MessageDialogButtons> for rfd::MessageButtons {
|
||||
|
@ -187,6 +191,10 @@ impl From<MessageDialogButtons> for rfd::MessageButtons {
|
|||
MessageDialogButtons::Ok => Self::Ok,
|
||||
MessageDialogButtons::OkCancel => Self::OkCancel,
|
||||
MessageDialogButtons::YesNo => Self::YesNo,
|
||||
MessageDialogButtons::OkWithLabel(ok_text) => Self::OkCustom(ok_text),
|
||||
MessageDialogButtons::OkCancelWithLabels(ok_text, cancel_text) => {
|
||||
Self::OkCancelCustom(ok_text, cancel_text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -226,7 +226,7 @@ mod test {
|
|||
fn check_test_dir() {
|
||||
// create a callback closure that takes in a TempDir type and prints it.
|
||||
let callback = |td: &tempfile::TempDir| {
|
||||
println!("{:?}", td);
|
||||
println!("{td:?}");
|
||||
};
|
||||
|
||||
// execute the with_temp_dir function on the callback
|
||||
|
|
|
@ -150,7 +150,7 @@ impl<'a, R: std::fmt::Debug + Read + Seek> std::fmt::Debug for Extract<'a, R> {
|
|||
impl<'a, R: Read + Seek> Extract<'a, R> {
|
||||
/// Create archive from reader.
|
||||
pub fn from_cursor(mut reader: R, archive_format: ArchiveFormat) -> Extract<'a, R> {
|
||||
if reader.seek(io::SeekFrom::Start(0)).is_err() {
|
||||
if reader.rewind().is_err() {
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!("Could not seek to start of the file");
|
||||
}
|
||||
|
|
|
@ -441,8 +441,7 @@ impl<'de> Deserialize<'de> for HeaderMap {
|
|||
headers.insert(key, value);
|
||||
} else {
|
||||
return Err(serde::de::Error::custom(format!(
|
||||
"invalid header `{}` `{}`",
|
||||
key, value
|
||||
"invalid header `{key}` `{value}`"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -242,22 +242,22 @@ mod test {
|
|||
}
|
||||
|
||||
let raw_str = "T".repeat(MIN_JSON_PARSE_LEN);
|
||||
assert_eq!(serialize_js(&raw_str).unwrap(), format!("\"{}\"", raw_str));
|
||||
assert_eq!(serialize_js(&raw_str).unwrap(), format!("\"{raw_str}\""));
|
||||
|
||||
assert_eq!(
|
||||
serialize_js(&JsonObj {
|
||||
value: raw_str.clone()
|
||||
})
|
||||
.unwrap(),
|
||||
format!("JSON.parse('{{\"value\":\"{}\"}}')", raw_str)
|
||||
format!("JSON.parse('{{\"value\":\"{raw_str}\"}}')")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serialize_js(&JsonObj {
|
||||
value: format!("\"{}\"", raw_str)
|
||||
value: format!("\"{raw_str}\"")
|
||||
})
|
||||
.unwrap(),
|
||||
format!("JSON.parse('{{\"value\":\"\\\\\"{}\\\\\"\"}}')", raw_str)
|
||||
format!("JSON.parse('{{\"value\":\"\\\\\"{raw_str}\\\\\"\"}}')")
|
||||
);
|
||||
|
||||
let dangerous_json = RawValue::from_string(
|
||||
|
|
|
@ -91,7 +91,7 @@ impl Program {
|
|||
|
||||
/// Opens path or URL with the program specified in `with`, or system default if `None`.
|
||||
///
|
||||
/// The path will be matched against the shell open validation regex, defaulting to `^https?://`.
|
||||
/// The path will be matched against the shell open validation regex, defaulting to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`.
|
||||
/// A custom validation regex may be supplied in the config in `tauri > allowlist > scope > open`.
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -112,5 +112,5 @@ pub fn open<P: AsRef<str>>(
|
|||
) -> crate::api::Result<()> {
|
||||
scope
|
||||
.open(path.as_ref(), with)
|
||||
.map_err(|err| crate::api::Error::Shell(format!("failed to open: {}", err)))
|
||||
.map_err(|err| crate::api::Error::Shell(format!("failed to open: {err}")))
|
||||
}
|
||||
|
|
|
@ -23,8 +23,8 @@ use crate::{
|
|||
sealed::{ManagerBase, RuntimeOrDispatch},
|
||||
utils::config::Config,
|
||||
utils::{assets::Assets, resources::resource_relpath, Env},
|
||||
Context, EventLoopMessage, Invoke, InvokeError, InvokeResponse, Manager, Runtime, Scopes,
|
||||
StateManager, Theme, Window,
|
||||
Context, DeviceEventFilter, EventLoopMessage, Invoke, InvokeError, InvokeResponse, Manager,
|
||||
Runtime, Scopes, StateManager, Theme, Window,
|
||||
};
|
||||
|
||||
#[cfg(shell_scope)]
|
||||
|
@ -805,6 +805,35 @@ impl<R: Runtime> App<R> {
|
|||
.set_activation_policy(activation_policy);
|
||||
}
|
||||
|
||||
/// Change the device event filter mode.
|
||||
///
|
||||
/// Since the DeviceEvent capture can lead to high CPU usage for unfocused windows, [`tao`]
|
||||
/// will ignore them by default for unfocused windows on Windows. This method allows changing
|
||||
/// the filter to explicitly capture them again.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - ** Linux / macOS / iOS / Android**: Unsupported.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```,no_run
|
||||
/// let mut app = tauri::Builder::default()
|
||||
/// // on an actual app, remove the string argument
|
||||
/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
|
||||
/// .expect("error while building tauri application");
|
||||
/// app.set_device_event_filter(tauri::DeviceEventFilter::Always);
|
||||
/// app.run(|_app_handle, _event| {});
|
||||
/// ```
|
||||
///
|
||||
/// [`tao`]: https://crates.io/crates/tao
|
||||
pub fn set_device_event_filter(&mut self, filter: DeviceEventFilter) {
|
||||
self
|
||||
.runtime
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.set_device_event_filter(filter);
|
||||
}
|
||||
|
||||
/// Gets the argument matches of the CLI definition configured in `tauri.conf.json`.
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -1008,6 +1037,9 @@ pub struct Builder<R: Runtime> {
|
|||
/// The updater configuration.
|
||||
#[cfg(updater)]
|
||||
updater_settings: UpdaterSettings,
|
||||
|
||||
/// The device event filter.
|
||||
device_event_filter: DeviceEventFilter,
|
||||
}
|
||||
|
||||
impl<R: Runtime> Builder<R> {
|
||||
|
@ -1036,6 +1068,7 @@ impl<R: Runtime> Builder<R> {
|
|||
system_tray_event_listeners: Vec::new(),
|
||||
#[cfg(updater)]
|
||||
updater_settings: Default::default(),
|
||||
device_event_filter: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1257,8 +1290,7 @@ impl<R: Runtime> Builder<R> {
|
|||
let type_name = std::any::type_name::<T>();
|
||||
assert!(
|
||||
self.state.set(state),
|
||||
"state for type '{}' is already being managed",
|
||||
type_name
|
||||
"state for type '{type_name}' is already being managed"
|
||||
);
|
||||
self
|
||||
}
|
||||
|
@ -1486,6 +1518,28 @@ impl<R: Runtime> Builder<R> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Change the device event filter mode.
|
||||
///
|
||||
/// Since the DeviceEvent capture can lead to high CPU usage for unfocused windows, [`tao`]
|
||||
/// will ignore them by default for unfocused windows on Windows. This method allows changing
|
||||
/// the filter to explicitly capture them again.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - ** Linux / macOS / iOS / Android**: Unsupported.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```,no_run
|
||||
/// tauri::Builder::default()
|
||||
/// .device_event_filter(tauri::DeviceEventFilter::Always);
|
||||
/// ```
|
||||
///
|
||||
/// [`tao`]: https://crates.io/crates/tao
|
||||
pub fn device_event_filter(mut self, filter: DeviceEventFilter) -> Self {
|
||||
self.device_event_filter = filter;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds the application.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn build<A: Assets>(mut self, context: Context<A>) -> crate::Result<App<R>> {
|
||||
|
@ -1519,6 +1573,9 @@ impl<R: Runtime> Builder<R> {
|
|||
if let Some(ua) = &config.user_agent {
|
||||
webview_attributes = webview_attributes.user_agent(&ua.to_string());
|
||||
}
|
||||
if let Some(args) = &config.additional_browser_args {
|
||||
webview_attributes = webview_attributes.additional_browser_args(&args.to_string());
|
||||
}
|
||||
if !config.file_drop_enabled {
|
||||
webview_attributes = webview_attributes.disable_file_drop_handler();
|
||||
}
|
||||
|
@ -1531,13 +1588,15 @@ impl<R: Runtime> Builder<R> {
|
|||
}
|
||||
|
||||
#[cfg(any(windows, target_os = "linux"))]
|
||||
let runtime = if self.runtime_any_thread {
|
||||
let mut runtime = if self.runtime_any_thread {
|
||||
R::new_any_thread()?
|
||||
} else {
|
||||
R::new()?
|
||||
};
|
||||
#[cfg(not(any(windows, target_os = "linux")))]
|
||||
let runtime = R::new()?;
|
||||
let mut runtime = R::new()?;
|
||||
|
||||
runtime.set_device_event_filter(self.device_event_filter);
|
||||
|
||||
let runtime_handle = runtime.handle();
|
||||
|
||||
|
|
|
@ -64,6 +64,7 @@ pub struct SystemTray {
|
|||
icon_as_template_set: bool,
|
||||
#[cfg(target_os = "macos")]
|
||||
title: Option<String>,
|
||||
tooltip: Option<String>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for SystemTray {
|
||||
|
@ -98,6 +99,7 @@ impl Default for SystemTray {
|
|||
menu_on_left_click_set: false,
|
||||
#[cfg(target_os = "macos")]
|
||||
title: None,
|
||||
tooltip: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -257,6 +259,29 @@ impl SystemTray {
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the tray icon tooltip.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Linux:** Unsupported
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use tauri::SystemTray;
|
||||
///
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(|app| {
|
||||
/// let tray_handle = SystemTray::new().with_tooltip("My App").build(app)?;
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn with_tooltip(mut self, tooltip: &str) -> Self {
|
||||
self.tooltip = Some(tooltip.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the event listener for this system tray.
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -414,6 +439,10 @@ impl SystemTray {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(tooltip) = self.tooltip {
|
||||
runtime_tray = runtime_tray.with_tooltip(&tooltip);
|
||||
}
|
||||
|
||||
let id = runtime_tray.id;
|
||||
let tray_handler = match manager.runtime() {
|
||||
RuntimeOrDispatch::Runtime(r) => r.system_tray(runtime_tray),
|
||||
|
@ -610,6 +639,15 @@ impl<R: Runtime> SystemTrayHandle<R> {
|
|||
self.inner.set_title(title).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Set the tooltip for this tray icon.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Linux:** Unsupported
|
||||
pub fn set_tooltip(&self, tooltip: &str) -> crate::Result<()> {
|
||||
self.inner.set_tooltip(tooltip).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Destroys this system tray.
|
||||
pub fn destroy(&self) -> crate::Result<()> {
|
||||
self.inner.destroy().map_err(Into::into)
|
||||
|
|
|
@ -238,8 +238,7 @@ pub(crate) fn handle<R: Runtime>(
|
|||
if let Some(unknown_variant_name) = s.next() {
|
||||
if unknown_variant_name == module {
|
||||
return resolver.reject(format!(
|
||||
"The `{}` module is not enabled. You must enable one of its APIs in the allowlist.",
|
||||
module
|
||||
"The `{module}` module is not enabled. You must enable one of its APIs in the allowlist."
|
||||
));
|
||||
} else if module == "Window" {
|
||||
return resolver.reject(window::into_allowlist_error(unknown_variant_name).to_string());
|
||||
|
|
|
@ -14,19 +14,21 @@ use tauri_macros::{command_enum, module_command_handler, CommandModule};
|
|||
use std::path::PathBuf;
|
||||
|
||||
macro_rules! message_dialog {
|
||||
($fn_name: ident, $allowlist: ident, $buttons: expr) => {
|
||||
($fn_name: ident, $allowlist: ident, $button_labels_type: ty, $buttons: expr) => {
|
||||
#[module_command_handler($allowlist)]
|
||||
fn $fn_name<R: Runtime>(
|
||||
context: InvokeContext<R>,
|
||||
title: Option<String>,
|
||||
message: String,
|
||||
level: Option<MessageDialogType>,
|
||||
button_labels: $button_labels_type,
|
||||
) -> super::Result<bool> {
|
||||
let determine_button = $buttons;
|
||||
let mut builder = crate::api::dialog::blocking::MessageDialogBuilder::new(
|
||||
title.unwrap_or_else(|| context.window.app_handle.package_info().name.clone()),
|
||||
message,
|
||||
)
|
||||
.buttons($buttons);
|
||||
.buttons(determine_button(button_labels));
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
{
|
||||
builder = builder.parent(&context.window);
|
||||
|
@ -139,6 +141,8 @@ pub enum Cmd {
|
|||
message: String,
|
||||
#[serde(rename = "type")]
|
||||
level: Option<MessageDialogType>,
|
||||
#[serde(rename = "buttonLabel")]
|
||||
button_label: Option<String>,
|
||||
},
|
||||
#[cmd(dialog_ask, "dialog > ask")]
|
||||
AskDialog {
|
||||
|
@ -146,6 +150,8 @@ pub enum Cmd {
|
|||
message: String,
|
||||
#[serde(rename = "type")]
|
||||
level: Option<MessageDialogType>,
|
||||
#[serde(rename = "buttonLabels")]
|
||||
button_label: Option<(String, String)>,
|
||||
},
|
||||
#[cmd(dialog_confirm, "dialog > confirm")]
|
||||
ConfirmDialog {
|
||||
|
@ -153,6 +159,8 @@ pub enum Cmd {
|
|||
message: String,
|
||||
#[serde(rename = "type")]
|
||||
level: Option<MessageDialogType>,
|
||||
#[serde(rename = "buttonLabels")]
|
||||
button_labels: Option<(String, String)>,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -255,19 +263,36 @@ impl Cmd {
|
|||
message_dialog!(
|
||||
message_dialog,
|
||||
dialog_message,
|
||||
crate::api::dialog::MessageDialogButtons::Ok
|
||||
Option<String>,
|
||||
|label: Option<String>| {
|
||||
label
|
||||
.map(crate::api::dialog::MessageDialogButtons::OkWithLabel)
|
||||
.unwrap_or(crate::api::dialog::MessageDialogButtons::Ok)
|
||||
}
|
||||
);
|
||||
|
||||
message_dialog!(
|
||||
ask_dialog,
|
||||
dialog_ask,
|
||||
crate::api::dialog::MessageDialogButtons::YesNo
|
||||
Option<(String, String)>,
|
||||
|labels: Option<(String, String)>| {
|
||||
labels
|
||||
.map(|(yes, no)| crate::api::dialog::MessageDialogButtons::OkCancelWithLabels(yes, no))
|
||||
.unwrap_or(crate::api::dialog::MessageDialogButtons::YesNo)
|
||||
}
|
||||
);
|
||||
|
||||
message_dialog!(
|
||||
confirm_dialog,
|
||||
dialog_confirm,
|
||||
crate::api::dialog::MessageDialogButtons::OkCancel
|
||||
Option<(String, String)>,
|
||||
|labels: Option<(String, String)>| {
|
||||
labels
|
||||
.map(|(ok, cancel)| {
|
||||
crate::api::dialog::MessageDialogButtons::OkCancelWithLabels(ok, cancel)
|
||||
})
|
||||
.unwrap_or(crate::api::dialog::MessageDialogButtons::OkCancel)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -138,7 +138,7 @@ impl Cmd {
|
|||
serde_json::to_string(&p)
|
||||
.map_err(|e| {
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!("{}", e);
|
||||
eprintln!("{e}");
|
||||
e
|
||||
})
|
||||
.ok()
|
||||
|
|
|
@ -135,7 +135,7 @@ impl Cmd {
|
|||
Ok(cmd) => cmd,
|
||||
Err(e) => {
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!("{}", e);
|
||||
eprintln!("{e}");
|
||||
return Err(crate::Error::ProgramNotAllowed(PathBuf::from(program)).into_anyhow());
|
||||
}
|
||||
}
|
||||
|
@ -154,7 +154,7 @@ impl Cmd {
|
|||
if let Some(encoding) = crate::api::process::Encoding::for_label(encoding.as_bytes()) {
|
||||
command = command.encoding(encoding);
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(format!("unknown encoding {}", encoding)));
|
||||
return Err(anyhow::anyhow!(format!("unknown encoding {encoding}")));
|
||||
}
|
||||
}
|
||||
let (mut rx, child) = command.spawn()?;
|
||||
|
|
|
@ -63,10 +63,12 @@ pub enum WindowManagerCmd {
|
|||
InnerSize,
|
||||
OuterSize,
|
||||
IsFullscreen,
|
||||
IsMinimized,
|
||||
IsMaximized,
|
||||
IsDecorated,
|
||||
IsResizable,
|
||||
IsVisible,
|
||||
Title,
|
||||
CurrentMonitor,
|
||||
PrimaryMonitor,
|
||||
AvailableMonitors,
|
||||
|
@ -101,6 +103,8 @@ pub enum WindowManagerCmd {
|
|||
#[cfg(window_set_always_on_top)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
SetAlwaysOnTop(bool),
|
||||
#[cfg(window_set_content_protected)]
|
||||
SetContentProtected(bool),
|
||||
#[cfg(window_set_size)]
|
||||
SetSize(Size),
|
||||
#[cfg(window_set_min_size)]
|
||||
|
@ -162,6 +166,9 @@ pub fn into_allowlist_error(variant: &str) -> crate::Error {
|
|||
"close" => crate::Error::ApiNotAllowlisted("window > close".to_string()),
|
||||
"setDecorations" => crate::Error::ApiNotAllowlisted("window > setDecorations".to_string()),
|
||||
"setAlwaysOnTop" => crate::Error::ApiNotAllowlisted("window > setAlwaysOnTop".to_string()),
|
||||
"setContentProtected" => {
|
||||
crate::Error::ApiNotAllowlisted("window > setContentProtected".to_string())
|
||||
}
|
||||
"setSize" => crate::Error::ApiNotAllowlisted("window > setSize".to_string()),
|
||||
"setMinSize" => crate::Error::ApiNotAllowlisted("window > setMinSize".to_string()),
|
||||
"setMaxSize" => crate::Error::ApiNotAllowlisted("window > setMaxSize".to_string()),
|
||||
|
@ -180,9 +187,10 @@ pub fn into_allowlist_error(variant: &str) -> crate::Error {
|
|||
}
|
||||
"startDragging" => crate::Error::ApiNotAllowlisted("window > startDragging".to_string()),
|
||||
"print" => crate::Error::ApiNotAllowlisted("window > print".to_string()),
|
||||
"internalToggleMaximize" => {
|
||||
"__toggleMaximize" => {
|
||||
crate::Error::ApiNotAllowlisted("window > maximize and window > unmaximize".to_string())
|
||||
}
|
||||
"__toggleDevtools" => crate::Error::ApiNotAllowlisted("devtools".to_string()),
|
||||
_ => crate::Error::ApiNotAllowlisted("window".to_string()),
|
||||
}
|
||||
}
|
||||
|
@ -253,10 +261,12 @@ impl Cmd {
|
|||
WindowManagerCmd::InnerSize => return Ok(window.inner_size()?.into()),
|
||||
WindowManagerCmd::OuterSize => return Ok(window.outer_size()?.into()),
|
||||
WindowManagerCmd::IsFullscreen => return Ok(window.is_fullscreen()?.into()),
|
||||
WindowManagerCmd::IsMinimized => return Ok(window.is_minimized()?.into()),
|
||||
WindowManagerCmd::IsMaximized => return Ok(window.is_maximized()?.into()),
|
||||
WindowManagerCmd::IsDecorated => return Ok(window.is_decorated()?.into()),
|
||||
WindowManagerCmd::IsResizable => return Ok(window.is_resizable()?.into()),
|
||||
WindowManagerCmd::IsVisible => return Ok(window.is_visible()?.into()),
|
||||
WindowManagerCmd::Title => return Ok(window.title()?.into()),
|
||||
WindowManagerCmd::CurrentMonitor => return Ok(window.current_monitor()?.into()),
|
||||
WindowManagerCmd::PrimaryMonitor => return Ok(window.primary_monitor()?.into()),
|
||||
WindowManagerCmd::AvailableMonitors => return Ok(window.available_monitors()?.into()),
|
||||
|
@ -295,6 +305,10 @@ impl Cmd {
|
|||
WindowManagerCmd::SetDecorations(decorations) => window.set_decorations(decorations)?,
|
||||
#[cfg(window_set_always_on_top)]
|
||||
WindowManagerCmd::SetAlwaysOnTop(always_on_top) => window.set_always_on_top(always_on_top)?,
|
||||
#[cfg(window_set_content_protected)]
|
||||
WindowManagerCmd::SetContentProtected(protected) => {
|
||||
window.set_content_protected(protected)?
|
||||
}
|
||||
#[cfg(window_set_size)]
|
||||
WindowManagerCmd::SetSize(size) => window.set_size(size)?,
|
||||
#[cfg(window_set_min_size)]
|
||||
|
|
|
@ -232,7 +232,7 @@ mod test {
|
|||
|
||||
// dummy event handler function
|
||||
fn event_fn(s: Event) {
|
||||
println!("{:?}", s);
|
||||
println!("{s:?}");
|
||||
}
|
||||
|
||||
proptest! {
|
||||
|
@ -304,18 +304,15 @@ pub fn unlisten_js(listeners_object_name: String, event_name: String, event_id:
|
|||
format!(
|
||||
"
|
||||
(function () {{
|
||||
const listeners = (window['{listeners}'] || {{}})['{event_name}']
|
||||
const listeners = (window['{listeners_object_name}'] || {{}})['{event_name}']
|
||||
if (listeners) {{
|
||||
const index = window['{listeners}']['{event_name}'].findIndex(e => e.id === {event_id})
|
||||
const index = window['{listeners_object_name}']['{event_name}'].findIndex(e => e.id === {event_id})
|
||||
if (index > -1) {{
|
||||
window['{listeners}']['{event_name}'].splice(index, 1)
|
||||
window['{listeners_object_name}']['{event_name}'].splice(index, 1)
|
||||
}}
|
||||
}}
|
||||
}})()
|
||||
",
|
||||
listeners = listeners_object_name,
|
||||
event_name = event_name,
|
||||
event_id = event_id,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -341,11 +338,7 @@ pub fn listen_js(
|
|||
windowLabel: {window_label},
|
||||
handler: {handler}
|
||||
}};
|
||||
if ({event} == 'tauri://window-created') {{
|
||||
eventListeners.splice(eventListeners.length - 1, 0, listener)
|
||||
}} else {{
|
||||
eventListeners.push(listener);
|
||||
}}
|
||||
eventListeners.push(listener);
|
||||
}})()
|
||||
",
|
||||
listeners = listeners_object_name,
|
||||
|
@ -353,7 +346,7 @@ pub fn listen_js(
|
|||
event_id = event_id,
|
||||
window_label = if let Some(l) = window_label {
|
||||
crate::runtime::window::assert_label_is_valid(&l);
|
||||
format!("'{}'", l)
|
||||
format!("'{l}'")
|
||||
} else {
|
||||
"null".to_owned()
|
||||
},
|
||||
|
|
|
@ -99,7 +99,7 @@ impl InvokeError {
|
|||
/// Create an [`InvokeError`] as a string of the [`anyhow::Error`] message.
|
||||
#[inline(always)]
|
||||
pub fn from_anyhow(error: anyhow::Error) -> Self {
|
||||
Self(JsonValue::String(format!("{:#}", error)))
|
||||
Self(JsonValue::String(format!("{error:#}")))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -134,6 +134,7 @@
|
|||
//! - **window-close**: Enables the [`close` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#close).
|
||||
//! - **window-set-decorations**: Enables the [`setDecorations` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setdecorations).
|
||||
//! - **window-set-always-on-top**: Enables the [`setAlwaysOnTop` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setalwaysontop).
|
||||
//! - **window-set-content-protected**: Enables the [`setContentProtected` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setcontentprotected).
|
||||
//! - **window-set-size**: Enables the [`setSize` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setsize).
|
||||
//! - **window-set-min-size**: Enables the [`setMinSize` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setminsize).
|
||||
//! - **window-set-max-size**: Enables the [`setMaxSize` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setmaxsize).
|
||||
|
@ -243,7 +244,7 @@ pub use {
|
|||
dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Pixel, Position, Size},
|
||||
CursorIcon, FileDropEvent,
|
||||
},
|
||||
RunIteration, UserAttentionType,
|
||||
DeviceEventFilter, RunIteration, UserAttentionType,
|
||||
},
|
||||
self::state::{State, StateManager},
|
||||
self::utils::{
|
||||
|
@ -439,8 +440,7 @@ impl TryFrom<Icon> for runtime::Icon {
|
|||
})
|
||||
}
|
||||
_ => panic!(
|
||||
"image `{}` extension not supported; please file a Tauri feature request. `png` or `ico` icons are supported with the `icon-png` and `icon-ico` feature flags",
|
||||
extension
|
||||
"image `{extension}` extension not supported; please file a Tauri feature request. `png` or `ico` icons are supported with the `icon-png` and `icon-ico` feature flags"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@ -844,8 +844,8 @@ mod tests {
|
|||
let lib_code = read_to_string(manifest_dir.join("src/lib.rs")).expect("failed to read lib.rs");
|
||||
|
||||
for f in get_manifest().features.keys() {
|
||||
if !(f.starts_with("__") || f == "default" || lib_code.contains(&format!("*{}**", f))) {
|
||||
panic!("Feature {} is not documented", f);
|
||||
if !(f.starts_with("__") || f == "default" || lib_code.contains(&format!("*{f}**"))) {
|
||||
panic!("Feature {f} is not documented");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -857,8 +857,7 @@ mod tests {
|
|||
for checked_feature in checked_features {
|
||||
if !manifest.features.iter().any(|(f, _)| f == checked_feature) {
|
||||
panic!(
|
||||
"Feature {} was checked in the alias build step but it does not exist in core/tauri/Cargo.toml",
|
||||
checked_feature
|
||||
"Feature {checked_feature} was checked in the alias build step but it does not exist in core/tauri/Cargo.toml"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -894,22 +893,21 @@ mod tests {
|
|||
let module = module_all_feature.replace("-all", "");
|
||||
assert!(
|
||||
checked_features.contains(&module_all_feature.as_str()),
|
||||
"`{}` is not aliased",
|
||||
module
|
||||
"`{module}` is not aliased"
|
||||
);
|
||||
|
||||
let module_prefix = format!("{}-", module);
|
||||
let module_prefix = format!("{module}-");
|
||||
// we assume that module features are the ones that start with `<module>-`
|
||||
// though it's not 100% accurate, we have an allowed list to fix it
|
||||
let module_features = manifest
|
||||
.features.keys()
|
||||
.features
|
||||
.keys()
|
||||
.filter(|f| f.starts_with(&module_prefix));
|
||||
for module_feature in module_features {
|
||||
assert!(
|
||||
allowed.contains(&module_feature.as_str())
|
||||
|| checked_features.contains(&module_feature.as_str()),
|
||||
"`{}` is not aliased",
|
||||
module_feature
|
||||
"`{module_feature}` is not aliased"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -937,7 +935,7 @@ mod test_utils {
|
|||
fn check_spawn_task(task in "[a-z]+") {
|
||||
// create dummy task function
|
||||
let dummy_task = async move {
|
||||
format!("{}-run-dummy-task", task);
|
||||
format!("{task}-run-dummy-task");
|
||||
};
|
||||
// call spawn
|
||||
crate::async_runtime::spawn(dummy_task);
|
||||
|
|
|
@ -184,7 +184,7 @@ fn replace_csp_nonce(
|
|||
if !(nonces.is_empty() && hashes.is_empty()) {
|
||||
let nonce_sources = nonces
|
||||
.into_iter()
|
||||
.map(|n| format!("'nonce-{}'", n))
|
||||
.map(|n| format!("'nonce-{n}'"))
|
||||
.collect::<Vec<String>>();
|
||||
let sources = csp.entry(directive.into()).or_insert_with(Default::default);
|
||||
let self_source = "'self'".to_string();
|
||||
|
@ -487,7 +487,7 @@ impl<R: Runtime> WindowManager<R> {
|
|||
window_url.scheme(),
|
||||
window_url.host().unwrap(),
|
||||
if let Some(port) = window_url.port() {
|
||||
format!(":{}", port)
|
||||
format!(":{port}")
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
|
@ -714,7 +714,7 @@ impl<R: Runtime> WindowManager<R> {
|
|||
{
|
||||
let assets = assets.clone();
|
||||
let schema_ = schema.clone();
|
||||
let url_base = format!("{}://localhost", schema_);
|
||||
let url_base = format!("{schema_}://localhost");
|
||||
let aes_gcm_key = *crypto_keys.aes_gcm().raw();
|
||||
|
||||
pending.register_uri_scheme_protocol(schema, move |request| {
|
||||
|
@ -813,7 +813,7 @@ impl<R: Runtime> WindowManager<R> {
|
|||
let asset_response = assets
|
||||
.get(&path.as_str().into())
|
||||
.or_else(|| {
|
||||
eprintln!("Asset `{}` not found; fallback to {}.html", path, path);
|
||||
eprintln!("Asset `{path}` not found; fallback to {path}.html");
|
||||
let fallback = format!("{}.html", path.as_str()).into();
|
||||
let asset = assets.get(&fallback);
|
||||
asset_path = fallback;
|
||||
|
@ -1302,6 +1302,14 @@ impl<R: Runtime> WindowManager<R> {
|
|||
.try_for_each(|window| window.emit_internal(event, source_window_label, payload.clone()))
|
||||
}
|
||||
|
||||
pub fn eval_script_all<S: Into<String>>(&self, script: S) -> crate::Result<()> {
|
||||
let script = script.into();
|
||||
self
|
||||
.windows_lock()
|
||||
.values()
|
||||
.try_for_each(|window| window.eval(&script))
|
||||
}
|
||||
|
||||
pub fn labels(&self) -> HashSet<String> {
|
||||
self.windows_lock().keys().cloned().collect()
|
||||
}
|
||||
|
@ -1408,8 +1416,7 @@ fn on_window_event<R: Runtime>(
|
|||
let windows = windows_map.values();
|
||||
for window in windows {
|
||||
window.eval(&format!(
|
||||
r#"(function () {{ const metadata = window.__TAURI_METADATA__; if (metadata != null) {{ metadata.__windows = window.__TAURI_METADATA__.__windows.filter(w => w.label !== "{}"); }} }})()"#,
|
||||
label
|
||||
r#"(function () {{ const metadata = window.__TAURI_METADATA__; if (metadata != null) {{ metadata.__windows = window.__TAURI_METADATA__.__windows.filter(w => w.label !== "{label}"); }} }})()"#
|
||||
))?;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,8 +87,8 @@ pub(crate) struct PatternJavascript {
|
|||
#[allow(dead_code)]
|
||||
pub(crate) fn format_real_schema(schema: &str) -> String {
|
||||
if cfg!(windows) {
|
||||
format!("https://{}.localhost", schema)
|
||||
format!("https://{schema}.localhost")
|
||||
} else {
|
||||
format!("{}://localhost", schema)
|
||||
format!("{schema}://localhost")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -545,7 +545,7 @@ impl<R: Runtime> PluginStore<R> {
|
|||
.values()
|
||||
.filter_map(|p| p.initialization_script())
|
||||
.fold(String::new(), |acc, script| {
|
||||
format!("{}\n(function () {{ {} }})();", acc, script)
|
||||
format!("{acc}\n(function () {{ {script} }})();")
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -586,9 +586,7 @@ impl<R: Runtime> PluginStore<R> {
|
|||
.unwrap_or_else(String::new);
|
||||
plugin.extend_api(invoke);
|
||||
} else {
|
||||
invoke
|
||||
.resolver
|
||||
.reject(format!("plugin {} not found", target));
|
||||
invoke.resolver.reject(format!("plugin {target} not found"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -141,7 +141,7 @@ 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 and subdirectories.
|
||||
/// 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) -> crate::Result<()> {
|
||||
let path = path.as_ref();
|
||||
{
|
||||
|
@ -216,13 +216,22 @@ impl Scope {
|
|||
|
||||
if let Ok(path) = path {
|
||||
let path: PathBuf = path.components().collect();
|
||||
let options = 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,
|
||||
// dotfiles are not supposed to be exposed by default
|
||||
#[cfg(unix)]
|
||||
require_literal_leading_dot: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let forbidden = self
|
||||
.forbidden_patterns
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|p| p.matches_path(&path));
|
||||
.any(|p| p.matches_path_with(&path, options));
|
||||
|
||||
if forbidden {
|
||||
false
|
||||
|
@ -232,7 +241,7 @@ impl Scope {
|
|||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|p| p.matches_path(&path));
|
||||
.any(|p| p.matches_path_with(&path, options));
|
||||
allowed
|
||||
}
|
||||
} else {
|
||||
|
@ -269,32 +278,97 @@ mod tests {
|
|||
#[test]
|
||||
fn path_is_escaped() {
|
||||
let scope = new_scope();
|
||||
scope.allow_directory("/home/tauri/**", false).unwrap();
|
||||
assert!(scope.is_allowed("/home/tauri/**"));
|
||||
assert!(scope.is_allowed("/home/tauri/**/file"));
|
||||
assert!(!scope.is_allowed("/home/tauri/anyfile"));
|
||||
#[cfg(unix)]
|
||||
{
|
||||
scope.allow_directory("/home/tauri/**", false).unwrap();
|
||||
assert!(scope.is_allowed("/home/tauri/**"));
|
||||
assert!(scope.is_allowed("/home/tauri/**/file"));
|
||||
assert!(!scope.is_allowed("/home/tauri/anyfile"));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
scope.allow_directory("C:\\home\\tauri\\**", false).unwrap();
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
|
||||
}
|
||||
|
||||
let scope = new_scope();
|
||||
scope.allow_file("/home/tauri/**").unwrap();
|
||||
assert!(scope.is_allowed("/home/tauri/**"));
|
||||
assert!(!scope.is_allowed("/home/tauri/**/file"));
|
||||
assert!(!scope.is_allowed("/home/tauri/anyfile"));
|
||||
#[cfg(unix)]
|
||||
{
|
||||
scope.allow_file("/home/tauri/**").unwrap();
|
||||
assert!(scope.is_allowed("/home/tauri/**"));
|
||||
assert!(!scope.is_allowed("/home/tauri/**/file"));
|
||||
assert!(!scope.is_allowed("/home/tauri/anyfile"));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
scope.allow_file("C:\\home\\tauri\\**").unwrap();
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
|
||||
}
|
||||
|
||||
let scope = new_scope();
|
||||
scope.allow_directory("/home/tauri", true).unwrap();
|
||||
scope.forbid_directory("/home/tauri/**", false).unwrap();
|
||||
assert!(!scope.is_allowed("/home/tauri/**"));
|
||||
assert!(!scope.is_allowed("/home/tauri/**/file"));
|
||||
assert!(!scope.is_allowed("/home/tauri/**/inner/file"));
|
||||
assert!(scope.is_allowed("/home/tauri/inner/folder/anyfile"));
|
||||
assert!(scope.is_allowed("/home/tauri/anyfile"));
|
||||
#[cfg(unix)]
|
||||
{
|
||||
scope.allow_directory("/home/tauri", true).unwrap();
|
||||
scope.forbid_directory("/home/tauri/**", false).unwrap();
|
||||
assert!(!scope.is_allowed("/home/tauri/**"));
|
||||
assert!(!scope.is_allowed("/home/tauri/**/file"));
|
||||
assert!(scope.is_allowed("/home/tauri/**/inner/file"));
|
||||
assert!(scope.is_allowed("/home/tauri/inner/folder/anyfile"));
|
||||
assert!(scope.is_allowed("/home/tauri/anyfile"));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
scope.allow_directory("C:\\home\\tauri", true).unwrap();
|
||||
scope
|
||||
.forbid_directory("C:\\home\\tauri\\**", false)
|
||||
.unwrap();
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\inner\\folder\\anyfile"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
|
||||
}
|
||||
|
||||
let scope = new_scope();
|
||||
scope.allow_directory("/home/tauri", true).unwrap();
|
||||
scope.forbid_file("/home/tauri/**").unwrap();
|
||||
assert!(!scope.is_allowed("/home/tauri/**"));
|
||||
assert!(scope.is_allowed("/home/tauri/**/file"));
|
||||
assert!(scope.is_allowed("/home/tauri/**/inner/file"));
|
||||
assert!(scope.is_allowed("/home/tauri/anyfile"));
|
||||
#[cfg(unix)]
|
||||
{
|
||||
scope.allow_directory("/home/tauri", true).unwrap();
|
||||
scope.forbid_file("/home/tauri/**").unwrap();
|
||||
assert!(!scope.is_allowed("/home/tauri/**"));
|
||||
assert!(scope.is_allowed("/home/tauri/**/file"));
|
||||
assert!(scope.is_allowed("/home/tauri/**/inner/file"));
|
||||
assert!(scope.is_allowed("/home/tauri/anyfile"));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
scope.allow_directory("C:\\home\\tauri", true).unwrap();
|
||||
scope.forbid_file("C:\\home\\tauri\\**").unwrap();
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
|
||||
}
|
||||
|
||||
let scope = new_scope();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
scope.allow_directory("/home/tauri", false).unwrap();
|
||||
assert!(scope.is_allowed("/home/tauri/**"));
|
||||
assert!(!scope.is_allowed("/home/tauri/**/file"));
|
||||
assert!(!scope.is_allowed("/home/tauri/**/inner/file"));
|
||||
assert!(scope.is_allowed("/home/tauri/anyfile"));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
scope.allow_directory("C:\\home\\tauri", false).unwrap();
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ impl Scope {
|
|||
.iter()
|
||||
.map(|url| {
|
||||
glob::Pattern::new(url.as_str())
|
||||
.unwrap_or_else(|_| panic!("scoped URL is not a valid glob pattern: `{}`", url))
|
||||
.unwrap_or_else(|_| panic!("scoped URL is not a valid glob pattern: `{url}`"))
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
|
|
|
@ -301,7 +301,7 @@ impl Scope {
|
|||
/// Open a path in the default (or specified) browser.
|
||||
///
|
||||
/// The path is validated against the `tauri > allowlist > shell > open` validation regex, which
|
||||
/// defaults to `^https?://`.
|
||||
/// defaults to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`.
|
||||
#[cfg(feature = "shell-open-api")]
|
||||
pub fn open(&self, path: &str, with: Option<Program>) -> Result<(), ScopeError> {
|
||||
// ensure we pass validation if the configuration has one
|
||||
|
|
|
@ -12,8 +12,8 @@ use tauri_runtime::{
|
|||
dpi::{PhysicalPosition, PhysicalSize, Position, Size},
|
||||
CursorIcon, DetachedWindow, MenuEvent, PendingWindow, WindowEvent,
|
||||
},
|
||||
Dispatch, EventLoopProxy, Icon, Result, RunEvent, Runtime, RuntimeHandle, UserAttentionType,
|
||||
UserEvent,
|
||||
DeviceEventFilter, Dispatch, EventLoopProxy, Icon, Result, RunEvent, Runtime, RuntimeHandle,
|
||||
UserAttentionType, UserEvent,
|
||||
};
|
||||
#[cfg(all(desktop, feature = "system-tray"))]
|
||||
use tauri_runtime::{
|
||||
|
@ -250,6 +250,10 @@ impl WindowBuilder for MockWindowBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
fn content_protected(self, protected: bool) -> Self {
|
||||
self
|
||||
}
|
||||
|
||||
fn icon(self, icon: Icon) -> Result<Self> {
|
||||
Ok(self)
|
||||
}
|
||||
|
@ -329,6 +333,10 @@ impl<T: UserEvent> Dispatch<T> for MockDispatcher {
|
|||
Ok(false)
|
||||
}
|
||||
|
||||
fn url(&self) -> Result<url::Url> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> Result<f64> {
|
||||
Ok(1.0)
|
||||
}
|
||||
|
@ -359,6 +367,10 @@ impl<T: UserEvent> Dispatch<T> for MockDispatcher {
|
|||
Ok(false)
|
||||
}
|
||||
|
||||
fn is_minimized(&self) -> Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn is_maximized(&self) -> Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
@ -375,6 +387,10 @@ impl<T: UserEvent> Dispatch<T> for MockDispatcher {
|
|||
Ok(true)
|
||||
}
|
||||
|
||||
fn title(&self) -> Result<String> {
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
fn is_menu_visible(&self) -> Result<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
|
@ -481,6 +497,10 @@ impl<T: UserEvent> Dispatch<T> for MockDispatcher {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn set_content_protected(&self, protected: bool) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_size(&self, size: Size) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
@ -574,6 +594,10 @@ impl TrayHandle for MockTrayHandler {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn set_tooltip(&self, tooltip: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn destroy(&self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
@ -696,6 +720,8 @@ impl<T: UserEvent> Runtime<T> for MockRuntime {
|
|||
#[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))]
|
||||
fn hide(&self) {}
|
||||
|
||||
fn set_device_event_filter(&mut self, filter: DeviceEventFilter) {}
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "macos",
|
||||
windows,
|
||||
|
|
|
@ -96,7 +96,7 @@ impl<'de> Deserialize<'de> for RemoteRelease {
|
|||
let pub_date = if let Some(date) = release.pub_date {
|
||||
Some(
|
||||
OffsetDateTime::parse(&date, &time::format_description::well_known::Rfc3339)
|
||||
.map_err(|e| DeError::custom(format!("invalid value for `pub_date`: {}", e)))?,
|
||||
.map_err(|e| DeError::custom(format!("invalid value for `pub_date`: {e}")))?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
|
@ -336,7 +336,7 @@ impl<R: Runtime> UpdateBuilder<R> {
|
|||
(target.clone(), target)
|
||||
} else {
|
||||
let target = get_updater_target().ok_or(Error::UnsupportedOs)?;
|
||||
(target.to_string(), format!("{}-{}", target, arch))
|
||||
(target.to_string(), format!("{target}-{arch}"))
|
||||
};
|
||||
|
||||
// Get the extract_path from the provided executable_path
|
||||
|
@ -612,15 +612,7 @@ impl<R: Runtime> Update<R> {
|
|||
archive_buffer,
|
||||
&self.extract_path,
|
||||
self.with_elevated_task,
|
||||
self
|
||||
.app
|
||||
.config()
|
||||
.tauri
|
||||
.updater
|
||||
.windows
|
||||
.install_mode
|
||||
.clone()
|
||||
.msiexec_args(),
|
||||
&self.app.config(),
|
||||
)?;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
copy_files_and_run(archive_buffer, &self.extract_path)?;
|
||||
|
@ -698,17 +690,19 @@ fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) ->
|
|||
}
|
||||
|
||||
// Windows
|
||||
|
||||
//
|
||||
// ### Expected structure:
|
||||
// ├── [AppName]_[version]_x64.msi.zip # ZIP generated by tauri-bundler
|
||||
// │ └──[AppName]_[version]_x64.msi # Application MSI
|
||||
// ├── [AppName]_[version]_x64-setup.exe.zip # ZIP generated by tauri-bundler
|
||||
// │ └──[AppName]_[version]_x64-setup.exe # NSIS installer
|
||||
// └── ...
|
||||
|
||||
//
|
||||
// ## MSI
|
||||
// Update server can provide a MSI for Windows. (Generated with tauri-bundler from *Wix*)
|
||||
// To replace current version of the application. In later version we'll offer
|
||||
// incremental update to push specific binaries.
|
||||
|
||||
//
|
||||
// ## EXE
|
||||
// Update server can provide a custom EXE (installer) who can run any task.
|
||||
#[cfg(target_os = "windows")]
|
||||
|
@ -717,7 +711,7 @@ fn copy_files_and_run<R: Read + Seek>(
|
|||
archive_buffer: R,
|
||||
_extract_path: &Path,
|
||||
with_elevated_task: bool,
|
||||
msiexec_args: &[&str],
|
||||
config: &crate::Config,
|
||||
) -> Result {
|
||||
// FIXME: We need to create a memory buffer with the MSI and then run it.
|
||||
// (instead of extracting the MSI to a temp path)
|
||||
|
@ -736,8 +730,6 @@ fn copy_files_and_run<R: Read + Seek>(
|
|||
extractor.extract_into(&tmp_dir)?;
|
||||
|
||||
let paths = read_dir(&tmp_dir)?;
|
||||
// This consumes the TempDir without deleting directory on the filesystem,
|
||||
// meaning that the directory will no longer be automatically deleted.
|
||||
|
||||
for path in paths {
|
||||
let found_path = path?.path();
|
||||
|
@ -745,9 +737,15 @@ fn copy_files_and_run<R: Read + Seek>(
|
|||
// If it's an `exe` we expect an installer not a runtime.
|
||||
if found_path.extension() == Some(OsStr::new("exe")) {
|
||||
// Run the EXE
|
||||
Command::new(found_path)
|
||||
.spawn()
|
||||
.expect("installer failed to start");
|
||||
let mut installer = Command::new(found_path);
|
||||
if crate::utils::config::WindowsUpdateInstallMode::Quiet
|
||||
== config.tauri.updater.windows.install_mode
|
||||
{
|
||||
installer.arg("/S");
|
||||
}
|
||||
installer.args(&config.tauri.updater.windows.installer_args);
|
||||
|
||||
installer.spawn().expect("installer failed to start");
|
||||
|
||||
exit(0);
|
||||
} else if found_path.extension() == Some(OsStr::new("msi")) {
|
||||
|
@ -801,6 +799,18 @@ fn copy_files_and_run<R: Read + Seek>(
|
|||
msi_path_arg.push(&found_path);
|
||||
msi_path_arg.push("\"\"\"");
|
||||
|
||||
let mut msiexec_args = config
|
||||
.tauri
|
||||
.updater
|
||||
.windows
|
||||
.install_mode
|
||||
.clone()
|
||||
.msiexec_args()
|
||||
.iter()
|
||||
.map(|p| p.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
msiexec_args.extend(config.tauri.updater.windows.installer_args.clone());
|
||||
|
||||
// run the installer and relaunch the application
|
||||
let system_root = std::env::var("SYSTEMROOT");
|
||||
let powershell_path = system_root.as_ref().map_or_else(
|
||||
|
@ -895,7 +905,7 @@ fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) ->
|
|||
})?;
|
||||
|
||||
let _ = std::process::Command::new("touch")
|
||||
.arg(&extract_path)
|
||||
.arg(extract_path)
|
||||
.status();
|
||||
|
||||
Ok(())
|
||||
|
@ -1049,14 +1059,13 @@ mod test {
|
|||
format!(
|
||||
r#"
|
||||
{{
|
||||
"name": "v{}",
|
||||
"name": "v{version}",
|
||||
"notes": "This is the latest version! Once updated you shouldn't see this prompt.",
|
||||
"pub_date": "2020-06-25T14:14:19Z",
|
||||
"signature": "{}",
|
||||
"url": "{}"
|
||||
"signature": "{public_signature}",
|
||||
"url": "{download_url}"
|
||||
}}
|
||||
"#,
|
||||
version, public_signature, download_url
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1069,15 +1078,14 @@ mod test {
|
|||
format!(
|
||||
r#"
|
||||
{{
|
||||
"name": "v{}",
|
||||
"name": "v{version}",
|
||||
"notes": "This is the latest version! Once updated you shouldn't see this prompt.",
|
||||
"pub_date": "2020-06-25T14:14:19Z",
|
||||
"signature": "{}",
|
||||
"url": "{}",
|
||||
"with_elevated_task": {}
|
||||
"signature": "{public_signature}",
|
||||
"url": "{download_url}",
|
||||
"with_elevated_task": {with_elevated_task}
|
||||
}}
|
||||
"#,
|
||||
version, public_signature, download_url, with_elevated_task
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1515,7 +1523,7 @@ mod test {
|
|||
}"#;
|
||||
|
||||
fn missing_field_error(field: &str) -> String {
|
||||
format!("the `{}` field was not set on the updater response", field)
|
||||
format!("the `{field}` field was not set on the updater response")
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
|
@ -1547,7 +1555,7 @@ mod test {
|
|||
.target("test-target")
|
||||
.build());
|
||||
if let Err(e) = check_update {
|
||||
println!("ERROR: {}, expected: {}", e, error);
|
||||
println!("ERROR: {e}, expected: {error}");
|
||||
assert!(e.to_string().contains(&error));
|
||||
} else {
|
||||
panic!("unexpected Ok response");
|
||||
|
|
|
@ -109,7 +109,7 @@ pub const EVENT_STATUS_UPTODATE: &str = "UPTODATE";
|
|||
/// Gets the target string used on the updater.
|
||||
pub fn target() -> Option<String> {
|
||||
if let (Some(target), Some(arch)) = (core::get_updater_target(), core::get_updater_arch()) {
|
||||
Some(format!("{}-{}", target, arch))
|
||||
Some(format!("{target}-{arch}"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -565,7 +565,7 @@ async fn prompt_for_install<R: Runtime>(
|
|||
// something more conventional.
|
||||
let should_install = ask(
|
||||
parent_window,
|
||||
format!(r#"A new version of {} is available! "#, app_name),
|
||||
format!(r#"A new version of {app_name} is available! "#),
|
||||
format!(
|
||||
r#"{} {} is now available -- you have {}.
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
pub(crate) mod menu;
|
||||
|
||||
pub use menu::{MenuEvent, MenuHandle};
|
||||
use url::Url;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::TitleBarStyle;
|
||||
|
@ -48,6 +49,7 @@ use std::{
|
|||
};
|
||||
|
||||
pub(crate) type WebResourceRequestHandler = dyn Fn(&HttpRequest, &mut HttpResponse) + Send + Sync;
|
||||
pub(crate) type NavigationHandler = dyn Fn(Url) -> bool + Send;
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
struct WindowCreatedEvent {
|
||||
|
@ -108,6 +110,7 @@ pub struct WindowBuilder<'a, R: Runtime> {
|
|||
pub(crate) window_builder: <R::Dispatcher as Dispatch<EventLoopMessage>>::WindowBuilder,
|
||||
pub(crate) webview_attributes: WebviewAttributes,
|
||||
web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
|
||||
navigation_handler: Option<Box<NavigationHandler>>,
|
||||
}
|
||||
|
||||
impl<'a, R: Runtime> fmt::Debug for WindowBuilder<'a, R> {
|
||||
|
@ -181,6 +184,7 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
|
|||
window_builder: <R::Dispatcher as Dispatch<EventLoopMessage>>::WindowBuilder::new(),
|
||||
webview_attributes: WebviewAttributes::new(url),
|
||||
web_resource_request_handler: None,
|
||||
navigation_handler: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,6 +233,33 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Defines a closure to be executed when the webview navigates to a URL. Returning `false` cancels the navigation.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::{
|
||||
/// utils::config::{Csp, CspDirectiveSources, WindowUrl},
|
||||
/// http::header::HeaderValue,
|
||||
/// window::WindowBuilder,
|
||||
/// };
|
||||
/// use std::collections::HashMap;
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(|app| {
|
||||
/// WindowBuilder::new(app, "core", WindowUrl::App("index.html".into()))
|
||||
/// .on_navigation(|url| {
|
||||
/// // allow the production URL or localhost on dev
|
||||
/// url.scheme() == "tauri" || (cfg!(dev) && url.host_str() == Some("localhost"))
|
||||
/// })
|
||||
/// .build()?;
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
pub fn on_navigation<F: Fn(Url) -> bool + Send + 'static>(mut self, f: F) -> Self {
|
||||
self.navigation_handler.replace(Box::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
/// Creates a new webview window.
|
||||
pub fn build(mut self) -> crate::Result<Window<R>> {
|
||||
let web_resource_request_handler = self.web_resource_request_handler.take();
|
||||
|
@ -238,12 +269,13 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
|
|||
self.label.clone(),
|
||||
)?;
|
||||
let labels = self.manager.labels().into_iter().collect::<Vec<_>>();
|
||||
let pending = self.manager.prepare_window(
|
||||
let mut pending = self.manager.prepare_window(
|
||||
self.app_handle.clone(),
|
||||
pending,
|
||||
&labels,
|
||||
web_resource_request_handler,
|
||||
)?;
|
||||
pending.navigation_handler = self.navigation_handler.take();
|
||||
let window = match &mut self.runtime {
|
||||
RuntimeOrDispatch::Runtime(runtime) => runtime.create_window(pending),
|
||||
RuntimeOrDispatch::RuntimeHandle(handle) => handle.create_window(pending),
|
||||
|
@ -251,6 +283,11 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
|
|||
}
|
||||
.map(|window| self.manager.attach_window(self.app_handle.clone(), window))?;
|
||||
|
||||
self.manager.eval_script_all(format!(
|
||||
"window.__TAURI_METADATA__.__windows = {window_labels_array}.map(function (label) {{ return {{ label: label }} }})",
|
||||
window_labels_array = serde_json::to_string(&self.manager.labels())?,
|
||||
))?;
|
||||
|
||||
self.manager.emit_filter(
|
||||
"tauri://window-created",
|
||||
None,
|
||||
|
@ -398,6 +435,13 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Whether the window should always be on top of other windows.
|
||||
#[must_use]
|
||||
pub fn content_protected(mut self, protected: bool) -> Self {
|
||||
self.window_builder = self.window_builder.content_protected(protected);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the window icon.
|
||||
pub fn icon(mut self, icon: Icon) -> crate::Result<Self> {
|
||||
self.window_builder = self.window_builder.icon(icon.try_into()?)?;
|
||||
|
@ -526,6 +570,22 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set additional arguments for the webview.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **macOS / Linux / Android / iOS**: Unsupported.
|
||||
///
|
||||
/// ## Warning
|
||||
///
|
||||
/// By default wry passes `--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection`
|
||||
/// so if you use this method, you also need to disable these components by yourself if you want.
|
||||
#[must_use]
|
||||
pub fn additional_browser_args(mut self, additional_args: &str) -> Self {
|
||||
self.webview_attributes.additional_browser_args = Some(additional_args.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Data directory for the webview.
|
||||
#[must_use]
|
||||
pub fn data_directory(mut self, data_directory: PathBuf) -> Self {
|
||||
|
@ -888,6 +948,11 @@ impl<R: Runtime> Window<R> {
|
|||
self.window.dispatcher.is_fullscreen().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Gets the window's current minimized state.
|
||||
pub fn is_minimized(&self) -> crate::Result<bool> {
|
||||
self.window.dispatcher.is_minimized().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Gets the window's current maximized state.
|
||||
pub fn is_maximized(&self) -> crate::Result<bool> {
|
||||
self.window.dispatcher.is_maximized().map_err(Into::into)
|
||||
|
@ -908,6 +973,11 @@ impl<R: Runtime> Window<R> {
|
|||
self.window.dispatcher.is_visible().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Gets the window's current title.
|
||||
pub fn title(&self) -> crate::Result<String> {
|
||||
self.window.dispatcher.title().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the monitor on which the window currently resides.
|
||||
///
|
||||
/// Returns None if current monitor can't be detected.
|
||||
|
@ -1115,6 +1185,15 @@ impl<R: Runtime> Window<R> {
|
|||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Prevents the window contents from being captured by other apps.
|
||||
pub fn set_content_protected(&self, protected: bool) -> crate::Result<()> {
|
||||
self
|
||||
.window
|
||||
.dispatcher
|
||||
.set_content_protected(protected)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Resizes this window.
|
||||
pub fn set_size<S: Into<Size>>(&self, size: S) -> crate::Result<()> {
|
||||
self
|
||||
|
@ -1256,6 +1335,11 @@ impl<R: Runtime> Window<R> {
|
|||
|
||||
/// Webview APIs.
|
||||
impl<R: Runtime> Window<R> {
|
||||
/// Returns the current url of the webview.
|
||||
pub fn url(&self) -> crate::Result<Url> {
|
||||
self.window.dispatcher.url().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Handles this window receiving an [`InvokeMessage`].
|
||||
pub fn on_message(self, payload: InvokePayload) -> crate::Result<()> {
|
||||
let manager = self.manager.clone();
|
||||
|
|
|
@ -8,6 +8,13 @@
|
|||
)]
|
||||
|
||||
fn main() {
|
||||
let mut context = tauri::generate_context!();
|
||||
if std::env::var("TARGET").unwrap_or_default() == "nsis" {
|
||||
context.config_mut().tauri.updater.windows.installer_args = vec![format!(
|
||||
"/D={}",
|
||||
std::env::current_exe().unwrap().parent().unwrap().display()
|
||||
)];
|
||||
}
|
||||
tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
let handle = app.handle();
|
||||
|
@ -15,19 +22,19 @@ fn main() {
|
|||
match handle.updater().check().await {
|
||||
Ok(update) => {
|
||||
if let Err(e) = update.download_and_install().await {
|
||||
println!("{}", e);
|
||||
println!("{e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
std::process::exit(0);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", e);
|
||||
println!("{e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.run(context)
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
|
|
@ -54,7 +54,13 @@ fn get_cli_bin_path(cli_dir: &Path, debug: bool) -> Option<PathBuf> {
|
|||
}
|
||||
}
|
||||
|
||||
fn build_app(cli_bin_path: &Path, cwd: &Path, config: &Config, bundle_updater: bool) {
|
||||
fn build_app(
|
||||
cli_bin_path: &Path,
|
||||
cwd: &Path,
|
||||
config: &Config,
|
||||
bundle_updater: bool,
|
||||
target: BundleTarget,
|
||||
) {
|
||||
let mut command = Command::new(cli_bin_path);
|
||||
command
|
||||
.args(["build", "--debug", "--verbose"])
|
||||
|
@ -62,18 +68,22 @@ fn build_app(cli_bin_path: &Path, cwd: &Path, config: &Config, bundle_updater: b
|
|||
.arg(serde_json::to_string(config).unwrap())
|
||||
.current_dir(cwd);
|
||||
|
||||
#[cfg(windows)]
|
||||
command.args(["--bundles", "msi"]);
|
||||
#[cfg(target_os = "linux")]
|
||||
command.args(["--bundles", "appimage"]);
|
||||
command.args(["--bundles", target.name()]);
|
||||
#[cfg(target_os = "macos")]
|
||||
command.args(["--bundles", "app"]);
|
||||
command.args(["--bundles", target.name()]);
|
||||
|
||||
if bundle_updater {
|
||||
#[cfg(windows)]
|
||||
command.args(["--bundles", "msi", "nsis"]);
|
||||
|
||||
command
|
||||
.env("TAURI_PRIVATE_KEY", UPDATER_PRIVATE_KEY)
|
||||
.env("TAURI_KEY_PASSWORD", "")
|
||||
.args(["--bundles", "updater"]);
|
||||
} else {
|
||||
#[cfg(windows)]
|
||||
command.args(["--bundles", target.name()]);
|
||||
}
|
||||
|
||||
let status = command
|
||||
|
@ -85,30 +95,83 @@ fn build_app(cli_bin_path: &Path, cwd: &Path, config: &Config, bundle_updater: b
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum BundleTarget {
|
||||
AppImage,
|
||||
|
||||
App,
|
||||
|
||||
Msi,
|
||||
Nsis,
|
||||
}
|
||||
|
||||
impl BundleTarget {
|
||||
fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::AppImage => "appimage",
|
||||
Self::App => "app",
|
||||
Self::Msi => "msi",
|
||||
Self::Nsis => "nsis",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BundleTarget {
|
||||
fn default() -> Self {
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
return Self::App;
|
||||
#[cfg(target_os = "linux")]
|
||||
return Self::App;
|
||||
#[cfg(windows)]
|
||||
return Self::Nsis;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn bundle_path(root_dir: &Path, version: &str) -> PathBuf {
|
||||
root_dir.join(format!(
|
||||
"target/debug/bundle/appimage/app-updater_{}_amd64.AppImage",
|
||||
version
|
||||
))
|
||||
fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> {
|
||||
vec![(
|
||||
BundleTarget::AppImage,
|
||||
root_dir.join(format!(
|
||||
"target/debug/bundle/appimage/app-updater_{}_amd64.AppImage",
|
||||
version
|
||||
)),
|
||||
)]
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn bundle_path(root_dir: &Path, _version: &str) -> PathBuf {
|
||||
root_dir.join(format!("target/debug/bundle/macos/app-updater.app"))
|
||||
fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> {
|
||||
vec![(
|
||||
BundleTarget::App,
|
||||
root_dir.join(format!("target/debug/bundle/macos/app-updater.app")),
|
||||
)]
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
fn bundle_path(root_dir: &Path, _version: &str) -> PathBuf {
|
||||
root_dir.join(format!("target/debug/bundle/ios/app-updater.app"))
|
||||
fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> {
|
||||
vec![(
|
||||
BundleTarget::App,
|
||||
root_dir.join(format!("target/debug/bundle/ios/app-updater.app")),
|
||||
)]
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn bundle_path(root_dir: &Path, version: &str) -> PathBuf {
|
||||
root_dir.join(format!(
|
||||
"target/debug/bundle/msi/app-updater_{}_x64_en-US.msi",
|
||||
version
|
||||
))
|
||||
fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> {
|
||||
vec![
|
||||
(
|
||||
BundleTarget::Nsis,
|
||||
root_dir.join(format!(
|
||||
"target/debug/bundle/nsis/app-updater_{}_x64-setup.exe",
|
||||
version
|
||||
)),
|
||||
),
|
||||
(
|
||||
BundleTarget::Msi,
|
||||
root_dir.join(format!(
|
||||
"target/debug/bundle/msi/app-updater_{}_x64_en-US.msi",
|
||||
version
|
||||
)),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -140,99 +203,118 @@ fn update_app() {
|
|||
};
|
||||
|
||||
// bundle app update
|
||||
build_app(&cli_bin_path, &manifest_dir, &config, true);
|
||||
build_app(
|
||||
&cli_bin_path,
|
||||
&manifest_dir,
|
||||
&config,
|
||||
true,
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
let updater_ext = if cfg!(windows) { "zip" } else { "tar.gz" };
|
||||
let updater_zip_ext = if cfg!(windows) { "zip" } else { "tar.gz" };
|
||||
|
||||
let out_bundle_path = bundle_path(&root_dir, "1.0.0");
|
||||
let signature_path = out_bundle_path.with_extension(format!(
|
||||
"{}.{}.sig",
|
||||
out_bundle_path.extension().unwrap().to_str().unwrap(),
|
||||
updater_ext
|
||||
));
|
||||
let signature = std::fs::read_to_string(&signature_path)
|
||||
.unwrap_or_else(|_| panic!("failed to read signature file {}", signature_path.display()));
|
||||
let out_updater_path = out_bundle_path.with_extension(format!(
|
||||
"{}.{}",
|
||||
out_bundle_path.extension().unwrap().to_str().unwrap(),
|
||||
updater_ext
|
||||
));
|
||||
let updater_path = root_dir.join(format!(
|
||||
"target/debug/{}",
|
||||
out_updater_path.file_name().unwrap().to_str().unwrap()
|
||||
));
|
||||
std::fs::rename(&out_updater_path, &updater_path).expect("failed to rename bundle");
|
||||
for (bundle_target, out_bundle_path) in bundle_paths(&root_dir, "1.0.0") {
|
||||
let bundle_updater_ext = out_bundle_path
|
||||
.extension()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.replace("exe", "nsis");
|
||||
let signature_path =
|
||||
out_bundle_path.with_extension(format!("{}.{}.sig", bundle_updater_ext, updater_zip_ext));
|
||||
let signature = std::fs::read_to_string(&signature_path)
|
||||
.unwrap_or_else(|_| panic!("failed to read signature file {}", signature_path.display()));
|
||||
let out_updater_path =
|
||||
out_bundle_path.with_extension(format!("{}.{}", bundle_updater_ext, updater_zip_ext));
|
||||
let updater_path = root_dir.join(format!(
|
||||
"target/debug/{}",
|
||||
out_updater_path.file_name().unwrap().to_str().unwrap()
|
||||
));
|
||||
std::fs::rename(&out_updater_path, &updater_path).expect("failed to rename bundle");
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// start the updater server
|
||||
let server = tiny_http::Server::http("localhost:3007").expect("failed to start updater server");
|
||||
let target = target.clone();
|
||||
std::thread::spawn(move || {
|
||||
// start the updater server
|
||||
let server =
|
||||
tiny_http::Server::http("localhost:3007").expect("failed to start updater server");
|
||||
|
||||
loop {
|
||||
if let Ok(request) = server.recv() {
|
||||
match request.url() {
|
||||
"/" => {
|
||||
let mut platforms = HashMap::new();
|
||||
loop {
|
||||
if let Ok(request) = server.recv() {
|
||||
match request.url() {
|
||||
"/" => {
|
||||
let mut platforms = HashMap::new();
|
||||
|
||||
platforms.insert(
|
||||
target.clone(),
|
||||
PlatformUpdate {
|
||||
signature: signature.clone(),
|
||||
url: "http://localhost:3007/download",
|
||||
with_elevated_task: false,
|
||||
},
|
||||
);
|
||||
let body = serde_json::to_vec(&Update {
|
||||
version: "1.0.0",
|
||||
date: time::OffsetDateTime::now_utc()
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.unwrap(),
|
||||
platforms,
|
||||
})
|
||||
.unwrap();
|
||||
let len = body.len();
|
||||
let response = tiny_http::Response::new(
|
||||
tiny_http::StatusCode(200),
|
||||
Vec::new(),
|
||||
std::io::Cursor::new(body),
|
||||
Some(len),
|
||||
None,
|
||||
);
|
||||
let _ = request.respond(response);
|
||||
platforms.insert(
|
||||
target.clone(),
|
||||
PlatformUpdate {
|
||||
signature: signature.clone(),
|
||||
url: "http://localhost:3007/download",
|
||||
with_elevated_task: false,
|
||||
},
|
||||
);
|
||||
let body = serde_json::to_vec(&Update {
|
||||
version: "1.0.0",
|
||||
date: time::OffsetDateTime::now_utc()
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.unwrap(),
|
||||
platforms,
|
||||
})
|
||||
.unwrap();
|
||||
let len = body.len();
|
||||
let response = tiny_http::Response::new(
|
||||
tiny_http::StatusCode(200),
|
||||
Vec::new(),
|
||||
std::io::Cursor::new(body),
|
||||
Some(len),
|
||||
None,
|
||||
);
|
||||
let _ = request.respond(response);
|
||||
}
|
||||
"/download" => {
|
||||
let _ = request.respond(tiny_http::Response::from_file(
|
||||
File::open(&updater_path).unwrap_or_else(|_| {
|
||||
panic!("failed to open updater bundle {}", updater_path.display())
|
||||
}),
|
||||
));
|
||||
// close server
|
||||
return;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
"/download" => {
|
||||
let _ = request.respond(tiny_http::Response::from_file(
|
||||
File::open(&updater_path).unwrap_or_else(|_| {
|
||||
panic!("failed to open updater bundle {}", updater_path.display())
|
||||
}),
|
||||
));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
config.package.version = "0.1.0";
|
||||
|
||||
// bundle initial app version
|
||||
build_app(&cli_bin_path, &manifest_dir, &config, false, bundle_target);
|
||||
|
||||
let mut binary_cmd = if cfg!(windows) {
|
||||
Command::new(root_dir.join("target/debug/app-updater.exe"))
|
||||
} else if cfg!(target_os = "macos") {
|
||||
Command::new(
|
||||
bundle_paths(&root_dir, "0.1.0")
|
||||
.first()
|
||||
.unwrap()
|
||||
.1
|
||||
.join("Contents/MacOS/app-updater"),
|
||||
)
|
||||
} else if std::env::var("CI").map(|v| v == "true").unwrap_or_default() {
|
||||
let mut c = Command::new("xvfb-run");
|
||||
c.arg("--auto-servernum")
|
||||
.arg(&bundle_paths(&root_dir, "0.1.0").first().unwrap().1);
|
||||
c
|
||||
} else {
|
||||
Command::new(&bundle_paths(&root_dir, "0.1.0").first().unwrap().1)
|
||||
};
|
||||
|
||||
binary_cmd.env("TARGET", bundle_target.name());
|
||||
|
||||
let status = binary_cmd.status().expect("failed to run app");
|
||||
|
||||
if !status.success() {
|
||||
panic!("failed to run app");
|
||||
}
|
||||
});
|
||||
|
||||
config.package.version = "0.1.0";
|
||||
|
||||
// bundle initial app version
|
||||
build_app(&cli_bin_path, &manifest_dir, &config, false);
|
||||
|
||||
let mut binary_cmd = if cfg!(windows) {
|
||||
Command::new(root_dir.join("target/debug/app-updater.exe"))
|
||||
} else if cfg!(target_os = "macos") {
|
||||
Command::new(bundle_path(&root_dir, "0.1.0").join("Contents/MacOS/app-updater"))
|
||||
} else if std::env::var("CI").map(|v| v == "true").unwrap_or_default() {
|
||||
let mut c = Command::new("xvfb-run");
|
||||
c.arg("--auto-servernum")
|
||||
.arg(bundle_path(&root_dir, "0.1.0"));
|
||||
c
|
||||
} else {
|
||||
Command::new(bundle_path(&root_dir, "0.1.0"))
|
||||
};
|
||||
|
||||
let status = binary_cmd.status().expect("failed to run app");
|
||||
|
||||
if !status.success() {
|
||||
panic!("failed to run app");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ fn main() {
|
|||
env.args.clear();
|
||||
tauri::api::process::restart(&env)
|
||||
}
|
||||
Some(invalid) => panic!("only argument `restart` is allowed, {} is invalid", invalid),
|
||||
Some(invalid) => panic!("only argument `restart` is allowed, {invalid} is invalid"),
|
||||
None => {}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ fn symlink_runner(create_symlinks: impl Fn(&Path) -> io::Result<Symlink>) -> Res
|
|||
if cfg!(windows) {
|
||||
compiled_binary.set_extension("exe");
|
||||
}
|
||||
println!("{:?}", compiled_binary);
|
||||
println!("{compiled_binary:?}");
|
||||
|
||||
// set up all the temporary file paths
|
||||
let temp = tempfile::TempDir::new()?;
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -8,12 +8,6 @@ version = "1.0.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "adler32"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
|
||||
|
||||
[[package]]
|
||||
name = "aead"
|
||||
version = "0.4.3"
|
||||
|
@ -188,6 +182,12 @@ version = "0.13.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
|
@ -635,16 +635,6 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate"
|
||||
version = "0.7.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4"
|
||||
dependencies = [
|
||||
"adler32",
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.17"
|
||||
|
@ -1348,12 +1338,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ico"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a4b3331534254a9b64095ae60d3dc2a8225a7a70229cd5888be127cdc1f6804"
|
||||
checksum = "031530fe562d8c8d71c0635013d6d155bbfe8ba0aa4b4d2d24ce8af6b71047bd"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"png 0.11.0",
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1431,15 +1421,6 @@ dependencies = [
|
|||
"cfb 0.7.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inflate"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5f9f47468e9a76a6452271efadc88fe865a82be91fe75e6c0c57b87ccea59d4"
|
||||
dependencies = [
|
||||
"adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
|
@ -1541,7 +1522,19 @@ checksum = "f995a3c8f2bc3dd52a18a583e90f9ec109c047fa1603a853e46bcda14d2e279d"
|
|||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"treediff",
|
||||
"treediff 3.0.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "json-patch"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e712e62827c382a77b87f590532febb1f8b2fdbc3eefa1ee37fe7281687075ef"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"treediff 4.0.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1891,17 +1884,6 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.1"
|
||||
|
@ -2290,7 +2272,7 @@ version = "1.3.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.13.1",
|
||||
"indexmap",
|
||||
"line-wrap",
|
||||
"serde",
|
||||
|
@ -2298,18 +2280,6 @@ dependencies = [
|
|||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0b0cabbbd20c2d7f06dbf015e06aad59b6ca3d9ed14848783e98af9aaf19925"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"deflate",
|
||||
"inflate",
|
||||
"num-iter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.7"
|
||||
|
@ -2565,7 +2535,7 @@ version = "0.11.12"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.13.1",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
|
@ -2761,18 +2731,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.147"
|
||||
version = "1.0.152"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
|
||||
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.147"
|
||||
version = "1.0.152"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852"
|
||||
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -2781,9 +2751,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.87"
|
||||
version = "1.0.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45"
|
||||
checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
|
||||
dependencies = [
|
||||
"itoa 1.0.4",
|
||||
"ryu",
|
||||
|
@ -3032,9 +3002,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.103"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
|
||||
checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -3069,9 +3039,8 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tao"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2845fd58915455c5faf2c9ac5d8a5ed43bd23ab57f0a67d63612936209eae74"
|
||||
version = "0.15.8"
|
||||
source = "git+https://github.com/tauri-apps/tao?branch=dev#b3aa3982d18a1ca2c8f08a135d7256b1aca46369"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cairo-rs",
|
||||
|
@ -3103,11 +3072,11 @@ dependencies = [
|
|||
"objc",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"paste",
|
||||
"png 0.17.7",
|
||||
"png",
|
||||
"raw-window-handle",
|
||||
"scopeguard",
|
||||
"serde",
|
||||
"tao-macros",
|
||||
"unicode-segmentation",
|
||||
"uuid 1.2.1",
|
||||
"windows 0.39.0",
|
||||
|
@ -3115,6 +3084,16 @@ dependencies = [
|
|||
"x11-dl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tao-macros"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/tauri-apps/tao?branch=dev#b3aa3982d18a1ca2c8f08a135d7256b1aca46369"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.38"
|
||||
|
@ -3128,11 +3107,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "1.2.0"
|
||||
version = "1.2.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"attohttpc",
|
||||
"base64",
|
||||
"base64 0.13.1",
|
||||
"bytes",
|
||||
"clap",
|
||||
"cocoa",
|
||||
|
@ -3157,7 +3136,7 @@ dependencies = [
|
|||
"os_info",
|
||||
"os_pipe",
|
||||
"percent-encoding",
|
||||
"png 0.17.7",
|
||||
"png",
|
||||
"rand 0.8.5",
|
||||
"raw-window-handle",
|
||||
"regex",
|
||||
|
@ -3190,12 +3169,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
"heck 0.4.0",
|
||||
"json-patch",
|
||||
"json-patch 0.3.0",
|
||||
"quote",
|
||||
"semver 1.0.14",
|
||||
"serde_json",
|
||||
|
@ -3206,14 +3185,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.20.0",
|
||||
"brotli",
|
||||
"ico",
|
||||
"json-patch",
|
||||
"json-patch 0.3.0",
|
||||
"plist",
|
||||
"png 0.17.7",
|
||||
"png",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
|
@ -3230,7 +3209,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
dependencies = [
|
||||
"heck 0.4.0",
|
||||
"proc-macro2",
|
||||
|
@ -3242,7 +3221,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "0.12.0"
|
||||
version = "0.12.1"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
|
@ -3253,6 +3232,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"tauri-utils",
|
||||
"thiserror",
|
||||
"url",
|
||||
"uuid 1.2.1",
|
||||
"webview2-com",
|
||||
"windows 0.39.0",
|
||||
|
@ -3260,7 +3240,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "0.12.0"
|
||||
version = "0.12.2"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"gtk",
|
||||
|
@ -3278,7 +3258,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"brotli",
|
||||
|
@ -3288,7 +3268,7 @@ dependencies = [
|
|||
"heck 0.4.0",
|
||||
"html5ever",
|
||||
"infer 0.7.0",
|
||||
"json-patch",
|
||||
"json-patch 0.2.6",
|
||||
"kuchiki",
|
||||
"memchr",
|
||||
"phf 0.10.1",
|
||||
|
@ -3573,6 +3553,15 @@ dependencies = [
|
|||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "treediff"
|
||||
version = "4.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52984d277bdf2a751072b5df30ec0377febdb02f7696d64c2d7d54630bac4303"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.3"
|
||||
|
@ -4241,11 +4230,10 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wry"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "923d297b203eae65b095af16c02978b7932be1968012b4da7138390edf34dea5"
|
||||
version = "0.23.4"
|
||||
source = "git+https://github.com/tauri-apps/wry?branch=dev#fca42a0730e75a142f7f354c6ac3f6d6a0f4711f"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.13.1",
|
||||
"block",
|
||||
"cocoa",
|
||||
"core-graphics",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue