From 84209f35643115c18dc4fb79085ca1861b8274d8 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Tue, 18 Mar 2025 12:46:08 -0300 Subject: [PATCH 1/6] fix(dialog): do not create file copy for save file picker on iOS On iOS the file picker returns a security scoped resource file path on the save() file picker: https://developer.apple.com/documentation/uikit/uidocumentpickerviewcontroller?language=objc#Work-with-external-documents this means we can't directly access it without calling [startAccessingSecurityScopedResource](https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc) this PR changes the plugin to not access the save file early, leaving that to the user and returning its actual path --- .../gen/apple/api.xcodeproj/project.pbxproj | 4 +- plugins/dialog/guest-js/index.ts | 8 +++ plugins/dialog/ios/Sources/DialogPlugin.swift | 36 +++++++++++-- .../ios/Sources/FilePickerController.swift | 53 +++---------------- plugins/dialog/src/lib.rs | 24 +++++++++ 5 files changed, 72 insertions(+), 53 deletions(-) diff --git a/examples/api/src-tauri/gen/apple/api.xcodeproj/project.pbxproj b/examples/api/src-tauri/gen/apple/api.xcodeproj/project.pbxproj index 64ef20a8..798ed66a 100644 --- a/examples/api/src-tauri/gen/apple/api.xcodeproj/project.pbxproj +++ b/examples/api/src-tauri/gen/apple/api.xcodeproj/project.pbxproj @@ -387,7 +387,7 @@ CODE_SIGN_ENTITLEMENTS = api_iOS/api_iOS.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = Q93MBH6S2F; + DEVELOPMENT_TEAM = "Q93MBH6S2F"; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphoneos*]" = "arm64-sim x86_64"; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; @@ -442,7 +442,7 @@ CODE_SIGN_ENTITLEMENTS = api_iOS/api_iOS.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = Q93MBH6S2F; + DEVELOPMENT_TEAM = "Q93MBH6S2F"; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphoneos*]" = "arm64-sim x86_64"; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; diff --git a/plugins/dialog/guest-js/index.ts b/plugins/dialog/guest-js/index.ts index 150be95a..7a30ca3c 100644 --- a/plugins/dialog/guest-js/index.ts +++ b/plugins/dialog/guest-js/index.ts @@ -156,6 +156,10 @@ type OpenDialogReturn = T['directory'] extends true * } * ``` * + * ## Platform-specific + * + * - **iOS**: Returns a copy of the file to bypass [security scoped resource](https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc). + * * @returns A promise resolving to the selected path(s) * * @since 2.0.0 @@ -190,6 +194,10 @@ async function open( * }); * ``` * + * #### Platform-specific + * + * - **iOS**: Returns a copy of the file to bypass [security scoped resource](https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc). + * * @returns A promise resolving to the selected path. * * @since 2.0.0 diff --git a/plugins/dialog/ios/Sources/DialogPlugin.swift b/plugins/dialog/ios/Sources/DialogPlugin.swift index b3f7e7da..9e7f3860 100644 --- a/plugins/dialog/ios/Sources/DialogPlugin.swift +++ b/plugins/dialog/ios/Sources/DialogPlugin.swift @@ -74,10 +74,18 @@ class DialogPlugin: Plugin { onFilePickerResult = { (event: FilePickerEvent) -> Void in switch event { case .selected(let urls): - invoke.resolve(["files": urls]) + do { + let temporaryUrls = try urls.map { try self.saveTemporaryFile($0) } + invoke.resolve(["files": temporaryUrls]) + } catch { + let message = "Failed to create a temporary copy of the file: \(error)" + Logger.error("\(message)") + invoke.reject(message) + } case .cancelled: invoke.resolve(["files": nil]) case .error(let error): + Logger.error("failed to pick file: \(error)") invoke.reject(error) } } @@ -153,6 +161,7 @@ class DialogPlugin: Plugin { case .cancelled: invoke.resolve(["file": nil]) case .error(let error): + Logger.error("failed to pick file to save: \(error)") invoke.reject(error) } } @@ -192,6 +201,27 @@ class DialogPlugin: Plugin { self.onFilePickerResult?(event) } + private func saveTemporaryFile(_ sourceUrl: URL) throws -> URL { + var directory = URL(fileURLWithPath: NSTemporaryDirectory()) + if let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask) + .first + { + directory = cachesDirectory + } + let targetUrl = directory.appendingPathComponent(sourceUrl.lastPathComponent) + do { + try deleteFile(targetUrl) + } + try FileManager.default.copyItem(at: sourceUrl, to: targetUrl) + return targetUrl + } + + private func deleteFile(_ url: URL) throws { + if FileManager.default.fileExists(atPath: url.path) { + try FileManager.default.removeItem(atPath: url.path) + } + } + @objc public func showMessageDialog(_ invoke: Invoke) throws { let manager = self.manager let args = try invoke.parseArgs(MessageDialogOptions.self) @@ -206,8 +236,6 @@ class DialogPlugin: Plugin { UIAlertAction( title: cancelButtonLabel, style: UIAlertAction.Style.default, handler: { (_) -> Void in - Logger.error("cancel") - invoke.resolve([ "value": false, "cancelled": false, @@ -221,8 +249,6 @@ class DialogPlugin: Plugin { UIAlertAction( title: okButtonLabel, style: UIAlertAction.Style.default, handler: { (_) -> Void in - Logger.error("ok") - invoke.resolve([ "value": true, "cancelled": false, diff --git a/plugins/dialog/ios/Sources/FilePickerController.swift b/plugins/dialog/ios/Sources/FilePickerController.swift index b2752f0b..20c129d8 100644 --- a/plugins/dialog/ios/Sources/FilePickerController.swift +++ b/plugins/dialog/ios/Sources/FilePickerController.swift @@ -95,35 +95,11 @@ public class FilePickerController: NSObject { return nil } } - - private func saveTemporaryFile(_ sourceUrl: URL) throws -> URL { - var directory = URL(fileURLWithPath: NSTemporaryDirectory()) - if let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first { - directory = cachesDirectory - } - let targetUrl = directory.appendingPathComponent(sourceUrl.lastPathComponent) - do { - try deleteFile(targetUrl) - } - try FileManager.default.copyItem(at: sourceUrl, to: targetUrl) - return targetUrl - } - - private func deleteFile(_ url: URL) throws { - if FileManager.default.fileExists(atPath: url.path) { - try FileManager.default.removeItem(atPath: url.path) - } - } } extension FilePickerController: UIDocumentPickerDelegate { public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { - do { - let temporaryUrls = try urls.map { try saveTemporaryFile($0) } - self.plugin.onFilePickerEvent(.selected(temporaryUrls)) - } catch { - self.plugin.onFilePickerEvent(.error("Failed to create a temporary copy of the file")) - } + self.plugin.onFilePickerEvent(.selected(urls)) } public func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { @@ -148,12 +124,7 @@ extension FilePickerController: UIImagePickerControllerDelegate, UINavigationCon public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { dismissViewController(picker) { if let url = info[.mediaURL] as? URL { - do { - let temporaryUrl = try self.saveTemporaryFile(url) - self.plugin.onFilePickerEvent(.selected([temporaryUrl])) - } catch { - self.plugin.onFilePickerEvent(.error("Failed to create a temporary copy of the file")) - } + self.plugin.onFilePickerEvent(.selected([url])) } else { self.plugin.onFilePickerEvent(.cancelled) } @@ -169,7 +140,7 @@ extension FilePickerController: PHPickerViewControllerDelegate { self.plugin.onFilePickerEvent(.cancelled) return } - var temporaryUrls: [URL] = [] + var urls: [URL] = [] var errorMessage: String? let dispatchGroup = DispatchGroup() for result in results { @@ -190,12 +161,7 @@ extension FilePickerController: PHPickerViewControllerDelegate { errorMessage = "Unknown error" return } - do { - let temporaryUrl = try self.saveTemporaryFile(url) - temporaryUrls.append(temporaryUrl) - } catch { - errorMessage = "Failed to create a temporary copy of the file" - } + urls.append(url) }) } else if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) { dispatchGroup.enter() @@ -211,12 +177,7 @@ extension FilePickerController: PHPickerViewControllerDelegate { errorMessage = "Unknown error" return } - do { - let temporaryUrl = try self.saveTemporaryFile(url) - temporaryUrls.append(temporaryUrl) - } catch { - errorMessage = "Failed to create a temporary copy of the file" - } + urls.append(url) }) } else { errorMessage = "Unsupported file type identifier" @@ -227,7 +188,7 @@ extension FilePickerController: PHPickerViewControllerDelegate { self.plugin.onFilePickerEvent(.error(errorMessage)) return } - self.plugin.onFilePickerEvent(.selected(temporaryUrls)) + self.plugin.onFilePickerEvent(.selected(urls)) } } -} \ No newline at end of file +} diff --git a/plugins/dialog/src/lib.rs b/plugins/dialog/src/lib.rs index 2ef1c1ea..d6c4d19f 100644 --- a/plugins/dialog/src/lib.rs +++ b/plugins/dialog/src/lib.rs @@ -440,6 +440,12 @@ impl FileDialogBuilder { /// Ok(()) /// }); /// ``` + /// + /// ## Platform-specific + /// + /// - **iOS**: Returns a copy of the file to bypass [security scoped resource]. + /// + /// [security scoped resource]: https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc pub fn pick_file) + Send + 'static>(self, f: F) { pick_file(self, f) } @@ -551,6 +557,12 @@ impl FileDialogBuilder { /// Ok(()) /// }); /// ``` + /// + /// ## Platform-specific + /// + /// - **iOS**: Returns a [security scoped resource] so you must request access before reading or writing to the file. + /// + /// [security scoped resource]: https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc pub fn save_file) + Send + 'static>(self, f: F) { save_file(self, f) } @@ -573,6 +585,12 @@ impl FileDialogBuilder { /// // the file path is `None` if the user closed the dialog /// } /// ``` + /// + /// ## Platform-specific + /// + /// - **iOS**: Returns a copy of the file to bypass [security scoped resource]. + /// + /// [security scoped resource]: https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc pub fn blocking_pick_file(self) -> Option { blocking_fn!(self, pick_file) } @@ -651,6 +669,12 @@ impl FileDialogBuilder { /// // the file path is `None` if the user closed the dialog /// } /// ``` + /// + /// ## Platform-specific + /// + /// - **iOS**: Returns a [security scoped resource] so you must request access before reading or writing to the file. + /// + /// [security scoped resource]: https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc pub fn blocking_save_file(self) -> Option { blocking_fn!(self, save_file) } From b4cfd60cda6fedbc61d5e844c02e712fe27c6452 Mon Sep 17 00:00:00 2001 From: nashaofu Date: Sun, 23 Mar 2025 20:47:09 +0800 Subject: [PATCH 2/6] feat: select file set startAccessingSecurityScopedResource --- plugins/dialog/ios/Sources/DialogPlugin.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/dialog/ios/Sources/DialogPlugin.swift b/plugins/dialog/ios/Sources/DialogPlugin.swift index 9e7f3860..8e8cd715 100644 --- a/plugins/dialog/ios/Sources/DialogPlugin.swift +++ b/plugins/dialog/ios/Sources/DialogPlugin.swift @@ -157,6 +157,7 @@ class DialogPlugin: Plugin { onFilePickerResult = { (event: FilePickerEvent) -> Void in switch event { case .selected(let urls): + urls.first?.startAccessingSecurityScopedResource() invoke.resolve(["file": urls.first!]) case .cancelled: invoke.resolve(["file": nil]) From 715e28d92252f0fa28c5ccd6f5ce66522b53f2aa Mon Sep 17 00:00:00 2001 From: nashaofu Date: Fri, 4 Jul 2025 09:20:21 +0800 Subject: [PATCH 3/6] chore: Add log information --- plugins/dialog/ios/Sources/DialogPlugin.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/dialog/ios/Sources/DialogPlugin.swift b/plugins/dialog/ios/Sources/DialogPlugin.swift index 8e8cd715..3f4b558d 100644 --- a/plugins/dialog/ios/Sources/DialogPlugin.swift +++ b/plugins/dialog/ios/Sources/DialogPlugin.swift @@ -157,7 +157,8 @@ class DialogPlugin: Plugin { onFilePickerResult = { (event: FilePickerEvent) -> Void in switch event { case .selected(let urls): - urls.first?.startAccessingSecurityScopedResource() + Logger.info("picked file to save: \(urls.first!)") + urls.first!.startAccessingSecurityScopedResource() invoke.resolve(["file": urls.first!]) case .cancelled: invoke.resolve(["file": nil]) From 1367c60fc99a7418138579cd421d7d10080bbc4d Mon Sep 17 00:00:00 2001 From: nashaofu <19303058+nashaofu@users.noreply.github.com> Date: Wed, 16 Jul 2025 09:45:44 +0800 Subject: [PATCH 4/6] feat: dialog add stop_accessing_path command --- plugins/dialog/build.rs | 2 +- plugins/dialog/guest-js/index.ts | 55 +++++++++++++++---- plugins/dialog/ios/Sources/DialogPlugin.swift | 41 ++++---------- .../commands/stop_accessing_path.toml | 13 +++++ .../permissions/autogenerated/reference.md | 27 +++++++++ plugins/dialog/permissions/default.toml | 1 + .../dialog/permissions/schemas/schema.json | 16 +++++- plugins/dialog/src/commands.rs | 6 ++ plugins/dialog/src/lib.rs | 16 +++++- plugins/dialog/src/mobile.rs | 19 ++++++- 10 files changed, 152 insertions(+), 44 deletions(-) create mode 100644 plugins/dialog/permissions/autogenerated/commands/stop_accessing_path.toml diff --git a/plugins/dialog/build.rs b/plugins/dialog/build.rs index 4b3bb871..edda5385 100644 --- a/plugins/dialog/build.rs +++ b/plugins/dialog/build.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -const COMMANDS: &[&str] = &["open", "save", "message", "ask", "confirm"]; +const COMMANDS: &[&str] = &["open", "save", "stop_accessing_path", "message", "ask", "confirm"]; fn main() { let result = tauri_plugin::Builder::new(COMMANDS) diff --git a/plugins/dialog/guest-js/index.ts b/plugins/dialog/guest-js/index.ts index 7a30ca3c..b80de382 100644 --- a/plugins/dialog/guest-js/index.ts +++ b/plugins/dialog/guest-js/index.ts @@ -4,6 +4,31 @@ import { invoke } from '@tauri-apps/api/core' +class Path { + public path: string + constructor(path: string) { + this.path = path + } + + destroy() { + return invoke('plugin:dialog|stop-accessing-path', { path: this.path }) + } + + toPath() { + return this.path + } + + toString() { + return this.toPath() + } + + toJSON() { + return { + path: this.path + } + } +} + /** * Extension filters for the file dialog. * @@ -100,13 +125,7 @@ interface ConfirmDialogOptions { cancelLabel?: string } -type OpenDialogReturn = T['directory'] extends true - ? T['multiple'] extends true - ? string[] | null - : string | null - : T['multiple'] extends true - ? string[] | null - : string | null +type OpenDialogReturn = T['multiple'] extends true ? Path[] | null : Path | null /** * Open a file/directory selection dialog. @@ -171,7 +190,17 @@ async function open( Object.freeze(options) } - return await invoke('plugin:dialog|open', { options }) + const path = await invoke('plugin:dialog|open', { options }) + + if (Array.isArray(path)) { + return path.map((p) => new Path(p)) + } + + if (!path) { + return null + } + + return new Path(path) } /** @@ -202,12 +231,18 @@ async function open( * * @since 2.0.0 */ -async function save(options: SaveDialogOptions = {}): Promise { +async function save(options: SaveDialogOptions = {}): Promise { if (typeof options === 'object') { Object.freeze(options) } - return await invoke('plugin:dialog|save', { options }) + const path = await invoke('plugin:dialog|save', { options }) + + if (!path) { + return null + } + + return new Path(path) } /** diff --git a/plugins/dialog/ios/Sources/DialogPlugin.swift b/plugins/dialog/ios/Sources/DialogPlugin.swift index 3f4b558d..a992b644 100644 --- a/plugins/dialog/ios/Sources/DialogPlugin.swift +++ b/plugins/dialog/ios/Sources/DialogPlugin.swift @@ -38,6 +38,10 @@ struct SaveFileDialogOptions: Decodable { var defaultPath: String? } +struct StopAccessingPathOptions: Decodable { + var path: URL +} + class DialogPlugin: Plugin { var filePickerController: FilePickerController! @@ -74,14 +78,8 @@ class DialogPlugin: Plugin { onFilePickerResult = { (event: FilePickerEvent) -> Void in switch event { case .selected(let urls): - do { - let temporaryUrls = try urls.map { try self.saveTemporaryFile($0) } - invoke.resolve(["files": temporaryUrls]) - } catch { - let message = "Failed to create a temporary copy of the file: \(error)" - Logger.error("\(message)") - invoke.reject(message) - } + urls.forEach { $0.startAccessingSecurityScopedResource() } + invoke.resolve(["files": urls]) case .cancelled: invoke.resolve(["files": nil]) case .error(let error): @@ -179,6 +177,12 @@ class DialogPlugin: Plugin { } } + @objc public func stopAccessingPath(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(StopAccessingPathOptions.self) + args.path.stopAccessingSecurityScopedResource() + invoke.resolve() + } + private func presentViewController(_ viewControllerToPresent: UIViewController) { self.manager.viewController?.present(viewControllerToPresent, animated: true, completion: nil) } @@ -203,27 +207,6 @@ class DialogPlugin: Plugin { self.onFilePickerResult?(event) } - private func saveTemporaryFile(_ sourceUrl: URL) throws -> URL { - var directory = URL(fileURLWithPath: NSTemporaryDirectory()) - if let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask) - .first - { - directory = cachesDirectory - } - let targetUrl = directory.appendingPathComponent(sourceUrl.lastPathComponent) - do { - try deleteFile(targetUrl) - } - try FileManager.default.copyItem(at: sourceUrl, to: targetUrl) - return targetUrl - } - - private func deleteFile(_ url: URL) throws { - if FileManager.default.fileExists(atPath: url.path) { - try FileManager.default.removeItem(atPath: url.path) - } - } - @objc public func showMessageDialog(_ invoke: Invoke) throws { let manager = self.manager let args = try invoke.parseArgs(MessageDialogOptions.self) diff --git a/plugins/dialog/permissions/autogenerated/commands/stop_accessing_path.toml b/plugins/dialog/permissions/autogenerated/commands/stop_accessing_path.toml new file mode 100644 index 00000000..5f47a5e6 --- /dev/null +++ b/plugins/dialog/permissions/autogenerated/commands/stop_accessing_path.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-stop-accessing-path" +description = "Enables the stop_accessing_path command without any pre-configured scope." +commands.allow = ["stop_accessing_path"] + +[[permission]] +identifier = "deny-stop-accessing-path" +description = "Denies the stop_accessing_path command without any pre-configured scope." +commands.deny = ["stop_accessing_path"] diff --git a/plugins/dialog/permissions/autogenerated/reference.md b/plugins/dialog/permissions/autogenerated/reference.md index 3bbd265b..e427d1bf 100644 --- a/plugins/dialog/permissions/autogenerated/reference.md +++ b/plugins/dialog/permissions/autogenerated/reference.md @@ -14,6 +14,7 @@ All dialog types are enabled. - `allow-message` - `allow-save` - `allow-open` +- `allow-stop-accessing-path` ## Permission Table @@ -151,6 +152,32 @@ Enables the save command without any pre-configured scope. Denies the save command without any pre-configured scope. + + + + + + +`dialog:allow-stop-accessing-path` + + + + +Enables the stop_accessing_path command without any pre-configured scope. + + + + + + + +`dialog:deny-stop-accessing-path` + + + + +Denies the stop_accessing_path command without any pre-configured scope. + diff --git a/plugins/dialog/permissions/default.toml b/plugins/dialog/permissions/default.toml index cc936d90..a927a54a 100644 --- a/plugins/dialog/permissions/default.toml +++ b/plugins/dialog/permissions/default.toml @@ -17,4 +17,5 @@ permissions = [ "allow-message", "allow-save", "allow-open", + "allow-stop-accessing-path" ] diff --git a/plugins/dialog/permissions/schemas/schema.json b/plugins/dialog/permissions/schemas/schema.json index b47417ec..1026e020 100644 --- a/plugins/dialog/permissions/schemas/schema.json +++ b/plugins/dialog/permissions/schemas/schema.json @@ -355,10 +355,22 @@ "markdownDescription": "Denies the save command without any pre-configured scope." }, { - "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "description": "Enables the stop_accessing_path command without any pre-configured scope.", + "type": "string", + "const": "allow-stop-accessing-path", + "markdownDescription": "Enables the stop_accessing_path command without any pre-configured scope." + }, + { + "description": "Denies the stop_accessing_path command without any pre-configured scope.", + "type": "string", + "const": "deny-stop-accessing-path", + "markdownDescription": "Denies the stop_accessing_path command without any pre-configured scope." + }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`\n- `allow-stop-accessing-path`", "type": "string", "const": "default", - "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`" + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`\n- `allow-stop-accessing-path`" } ] } diff --git a/plugins/dialog/src/commands.rs b/plugins/dialog/src/commands.rs index c3caf027..011f3c76 100644 --- a/plugins/dialog/src/commands.rs +++ b/plugins/dialog/src/commands.rs @@ -9,6 +9,7 @@ use tauri::{command, Manager, Runtime, State, Window}; use tauri_plugin_fs::FsExt; use crate::{ + StopAccessingPath, Dialog, FileDialogBuilder, FilePath, MessageDialogButtons, MessageDialogKind, Result, CANCEL, NO, OK, YES, }; @@ -241,6 +242,11 @@ pub(crate) async fn save( Ok(path.map(|p| p.simplified())) } +#[command] +pub fn stop_accessing_path(_p: StopAccessingPath) -> bool { + true +} + fn message_dialog( #[allow(unused_variables)] window: Window, dialog: State<'_, Dialog>, diff --git a/plugins/dialog/src/lib.rs b/plugins/dialog/src/lib.rs index d6c4d19f..dd92476d 100644 --- a/plugins/dialog/src/lib.rs +++ b/plugins/dialog/src/lib.rs @@ -9,7 +9,7 @@ html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" )] -use serde::Serialize; +use serde::{Deserialize, Serialize}; use tauri::{ plugin::{Builder, TauriPlugin}, Manager, Runtime, @@ -180,6 +180,7 @@ pub fn init() -> TauriPlugin { .invoke_handler(tauri::generate_handler![ commands::open, commands::save, + commands::stop_accessing_path, commands::message, commands::ask, commands::confirm @@ -337,6 +338,11 @@ pub(crate) struct FileDialogPayload<'a> { multiple: bool, } +#[derive(Debug, Serialize, Deserialize)] +pub struct StopAccessingPath { + path: FilePath, +} + // raw window handle :( unsafe impl Send for FileDialogBuilder {} @@ -566,6 +572,10 @@ impl FileDialogBuilder { pub fn save_file) + Send + 'static>(self, f: F) { save_file(self, f) } + + pub fn stop_accessing_path(self, p: StopAccessingPath) -> bool { + stop_accessing_path(self, p) + } } /// Blocking APIs. @@ -678,4 +688,8 @@ impl FileDialogBuilder { pub fn blocking_save_file(self) -> Option { blocking_fn!(self, save_file) } + + pub fn blocking_stop_accessing_path(self, p: StopAccessingPath) -> bool { + self.stop_accessing_path(p) + } } diff --git a/plugins/dialog/src/mobile.rs b/plugins/dialog/src/mobile.rs index b73def4f..fb434b27 100644 --- a/plugins/dialog/src/mobile.rs +++ b/plugins/dialog/src/mobile.rs @@ -8,7 +8,7 @@ use tauri::{ AppHandle, Runtime, }; -use crate::{FileDialogBuilder, FilePath, MessageDialogBuilder}; +use crate::{FileDialogBuilder, FilePath, MessageDialogBuilder, StopAccessingPath}; #[cfg(target_os = "android")] const PLUGIN_IDENTIFIER: &str = "app.tauri.dialog"; @@ -105,6 +105,23 @@ pub fn save_file) + Send + 'static>( }); } +#[allow(unused_variables)] +pub fn stop_accessing_path(dialog: FileDialogBuilder, p: StopAccessingPath) -> bool { + #[cfg(target_os = "ios")] + { + let res = dialog + .dialog + .0 + .run_mobile_plugin::<()>("stopAccessingPath", p); + + if let Err(_) = res { + return false + } + } + + true +} + #[derive(Debug, Deserialize)] struct ShowMessageDialogResponse { #[allow(dead_code)] From 425aea08a73c2285296f7b85e41a84331071ec96 Mon Sep 17 00:00:00 2001 From: nashaofu Date: Thu, 17 Jul 2025 00:30:45 +0800 Subject: [PATCH 5/6] chore: format code --- plugins/dialog/build.rs | 9 ++++++++- plugins/dialog/src/commands.rs | 5 ++--- plugins/dialog/src/mobile.rs | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/plugins/dialog/build.rs b/plugins/dialog/build.rs index edda5385..0d575ce7 100644 --- a/plugins/dialog/build.rs +++ b/plugins/dialog/build.rs @@ -2,7 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -const COMMANDS: &[&str] = &["open", "save", "stop_accessing_path", "message", "ask", "confirm"]; +const COMMANDS: &[&str] = &[ + "open", + "save", + "stop_accessing_path", + "message", + "ask", + "confirm", +]; fn main() { let result = tauri_plugin::Builder::new(COMMANDS) diff --git a/plugins/dialog/src/commands.rs b/plugins/dialog/src/commands.rs index 011f3c76..97375bc3 100644 --- a/plugins/dialog/src/commands.rs +++ b/plugins/dialog/src/commands.rs @@ -9,9 +9,8 @@ use tauri::{command, Manager, Runtime, State, Window}; use tauri_plugin_fs::FsExt; use crate::{ - StopAccessingPath, - Dialog, FileDialogBuilder, FilePath, MessageDialogButtons, MessageDialogKind, Result, CANCEL, - NO, OK, YES, + Dialog, FileDialogBuilder, FilePath, MessageDialogButtons, MessageDialogKind, Result, + StopAccessingPath, CANCEL, NO, OK, YES, }; #[derive(Serialize)] diff --git a/plugins/dialog/src/mobile.rs b/plugins/dialog/src/mobile.rs index fb434b27..55c6d460 100644 --- a/plugins/dialog/src/mobile.rs +++ b/plugins/dialog/src/mobile.rs @@ -115,7 +115,7 @@ pub fn stop_accessing_path(dialog: FileDialogBuilder, p: StopAcce .run_mobile_plugin::<()>("stopAccessingPath", p); if let Err(_) = res { - return false + return false; } } From 86253d1de4297a65f3fe04bd78d0fe23412e5abc Mon Sep 17 00:00:00 2001 From: nashaofu Date: Thu, 17 Jul 2025 00:34:49 +0800 Subject: [PATCH 6/6] fix: build failed --- plugins/dialog/src/desktop.rs | 9 ++++++++- plugins/dialog/src/mobile.rs | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/dialog/src/desktop.rs b/plugins/dialog/src/desktop.rs index d1a3e8b2..aeadf6c7 100644 --- a/plugins/dialog/src/desktop.rs +++ b/plugins/dialog/src/desktop.rs @@ -13,7 +13,7 @@ use rfd::{AsyncFileDialog, AsyncMessageDialog}; use serde::de::DeserializeOwned; use tauri::{plugin::PluginApi, AppHandle, Runtime}; -use crate::{models::*, FileDialogBuilder, FilePath, MessageDialogBuilder, OK}; +use crate::{models::*, FileDialogBuilder, FilePath, MessageDialogBuilder, StopAccessingPath, OK}; pub fn init( app: &AppHandle, @@ -207,6 +207,13 @@ pub fn save_file) + Send + 'static>( }); } +pub fn stop_accessing_path( + _dialog: FileDialogBuilder, + _p: StopAccessingPath, +) -> bool { + true +} + /// Shows a message dialog pub fn show_message_dialog( dialog: MessageDialogBuilder, diff --git a/plugins/dialog/src/mobile.rs b/plugins/dialog/src/mobile.rs index 55c6d460..d4906a40 100644 --- a/plugins/dialog/src/mobile.rs +++ b/plugins/dialog/src/mobile.rs @@ -114,7 +114,7 @@ pub fn stop_accessing_path(dialog: FileDialogBuilder, p: StopAcce .0 .run_mobile_plugin::<()>("stopAccessingPath", p); - if let Err(_) = res { + if res.is_err() { return false; } }