mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-06-08 14:03:53 +02:00
feat(dialog): implement save API on iOS (#1707)
This commit is contained in:
committed by
GitHub
parent
ff134a8ca4
commit
feb1e93fcb
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"dialog": patch:feat
|
||||
---
|
||||
|
||||
Implement `save` API on iOS.
|
||||
@@ -17,10 +17,10 @@ enum FilePickerEvent {
|
||||
}
|
||||
|
||||
struct MessageDialogOptions: Decodable {
|
||||
let title: String?
|
||||
var title: String?
|
||||
let message: String
|
||||
let okButtonLabel: String?
|
||||
let cancelButtonLabel: String?
|
||||
var okButtonLabel: String?
|
||||
var cancelButtonLabel: String?
|
||||
}
|
||||
|
||||
struct Filter: Decodable {
|
||||
@@ -30,13 +30,18 @@ struct Filter: Decodable {
|
||||
struct FilePickerOptions: Decodable {
|
||||
var multiple: Bool?
|
||||
var filters: [Filter]?
|
||||
var defaultPath: String?
|
||||
}
|
||||
|
||||
struct SaveFileDialogOptions: Decodable {
|
||||
var fileName: String?
|
||||
var defaultPath: String?
|
||||
}
|
||||
|
||||
class DialogPlugin: Plugin {
|
||||
|
||||
var filePickerController: FilePickerController!
|
||||
var pendingInvoke: Invoke? = nil
|
||||
var pendingInvokeArgs: FilePickerOptions? = nil
|
||||
var onFilePickerResult: ((FilePickerEvent) -> Void)? = nil
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
@@ -66,8 +71,16 @@ class DialogPlugin: Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
pendingInvoke = invoke
|
||||
pendingInvokeArgs = args
|
||||
onFilePickerResult = { (event: FilePickerEvent) -> Void in
|
||||
switch event {
|
||||
case .selected(let urls):
|
||||
invoke.resolve(["files": urls])
|
||||
case .cancelled:
|
||||
invoke.resolve(["files": nil])
|
||||
case .error(let error):
|
||||
invoke.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
if uniqueMimeType == true || isMedia {
|
||||
DispatchQueue.main.async {
|
||||
@@ -104,6 +117,9 @@ class DialogPlugin: Plugin {
|
||||
let documentTypes = parsedTypes.isEmpty ? ["public.data"] : parsedTypes
|
||||
DispatchQueue.main.async {
|
||||
let picker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import)
|
||||
if let defaultPath = args.defaultPath {
|
||||
picker.directoryURL = URL(string: defaultPath)
|
||||
}
|
||||
picker.delegate = self.filePickerController
|
||||
picker.allowsMultipleSelection = args.multiple ?? false
|
||||
picker.modalPresentationStyle = .fullScreen
|
||||
@@ -112,6 +128,46 @@ class DialogPlugin: Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func saveFileDialog(_ invoke: Invoke) throws {
|
||||
let args = try invoke.parseArgs(SaveFileDialogOptions.self)
|
||||
|
||||
// The Tauri save dialog API prompts the user to select a path where a file must be saved
|
||||
// This behavior maps to the operating system interfaces on all platforms except iOS,
|
||||
// which only exposes a mechanism to "move file `srcPath` to a location defined by the user"
|
||||
//
|
||||
// so we have to work around it by creating an empty file matching the requested `args.fileName`,
|
||||
// and using it as `srcPath` for the operation - returning the path the user selected
|
||||
// so the app dev can write to it later - matching cross platform behavior as mentioned above
|
||||
let fileManager = FileManager.default
|
||||
let srcFolder = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
let srcPath = srcFolder.appendingPathComponent(args.fileName ?? "file")
|
||||
if !fileManager.fileExists(atPath: srcPath.path) {
|
||||
// the file contents must be actually provided by the tauri dev after the path is resolved by the save API
|
||||
try "".write(to: srcPath, atomically: true, encoding: .utf8)
|
||||
}
|
||||
|
||||
onFilePickerResult = { (event: FilePickerEvent) -> Void in
|
||||
switch event {
|
||||
case .selected(let urls):
|
||||
invoke.resolve(["file": urls.first!])
|
||||
case .cancelled:
|
||||
invoke.resolve(["file": nil])
|
||||
case .error(let error):
|
||||
invoke.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let picker = UIDocumentPickerViewController(url: srcPath, in: .exportToService)
|
||||
if let defaultPath = args.defaultPath {
|
||||
picker.directoryURL = URL(string: defaultPath)
|
||||
}
|
||||
picker.delegate = self.filePickerController
|
||||
picker.modalPresentationStyle = .fullScreen
|
||||
self.presentViewController(picker)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentViewController(_ viewControllerToPresent: UIViewController) {
|
||||
self.manager.viewController?.present(viewControllerToPresent, animated: true, completion: nil)
|
||||
}
|
||||
@@ -133,14 +189,7 @@ class DialogPlugin: Plugin {
|
||||
}
|
||||
|
||||
public func onFilePickerEvent(_ event: FilePickerEvent) {
|
||||
switch event {
|
||||
case .selected(let urls):
|
||||
pendingInvoke?.resolve(["files": urls])
|
||||
case .cancelled:
|
||||
pendingInvoke?.resolve(["files": nil])
|
||||
case .error(let error):
|
||||
pendingInvoke?.reject(error)
|
||||
}
|
||||
self.onFilePickerResult?(event)
|
||||
}
|
||||
|
||||
@objc public func showMessageDialog(_ invoke: Invoke) throws {
|
||||
|
||||
@@ -197,41 +197,36 @@ pub(crate) async fn save<R: Runtime>(
|
||||
dialog: State<'_, Dialog<R>>,
|
||||
options: SaveDialogOptions,
|
||||
) -> Result<Option<FilePath>> {
|
||||
#[cfg(target_os = "ios")]
|
||||
return Err(crate::Error::FileSaveDialogNotImplemented);
|
||||
#[cfg(any(desktop, target_os = "android"))]
|
||||
let mut dialog_builder = dialog.file();
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
{
|
||||
let mut dialog_builder = dialog.file();
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
{
|
||||
dialog_builder = dialog_builder.set_parent(&window);
|
||||
}
|
||||
if let Some(title) = options.title {
|
||||
dialog_builder = dialog_builder.set_title(title);
|
||||
}
|
||||
if let Some(default_path) = options.default_path {
|
||||
dialog_builder = set_default_path(dialog_builder, default_path);
|
||||
}
|
||||
if let Some(can) = options.can_create_directories {
|
||||
dialog_builder = dialog_builder.set_can_create_directories(can);
|
||||
}
|
||||
for filter in options.filters {
|
||||
let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect();
|
||||
dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
|
||||
}
|
||||
|
||||
let path = dialog_builder.blocking_save_file();
|
||||
if let Some(p) = &path {
|
||||
if let Ok(path) = p.path() {
|
||||
if let Some(s) = window.try_fs_scope() {
|
||||
s.allow_file(&path);
|
||||
}
|
||||
window.state::<tauri::scope::Scopes>().allow_file(&path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(path.map(|p| p.simplified()))
|
||||
dialog_builder = dialog_builder.set_parent(&window);
|
||||
}
|
||||
if let Some(title) = options.title {
|
||||
dialog_builder = dialog_builder.set_title(title);
|
||||
}
|
||||
if let Some(default_path) = options.default_path {
|
||||
dialog_builder = set_default_path(dialog_builder, default_path);
|
||||
}
|
||||
if let Some(can) = options.can_create_directories {
|
||||
dialog_builder = dialog_builder.set_can_create_directories(can);
|
||||
}
|
||||
for filter in options.filters {
|
||||
let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect();
|
||||
dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
|
||||
}
|
||||
|
||||
let path = dialog_builder.blocking_save_file();
|
||||
if let Some(p) = &path {
|
||||
if let Ok(path) = p.path() {
|
||||
if let Some(s) = window.try_fs_scope() {
|
||||
s.allow_file(&path);
|
||||
}
|
||||
window.state::<tauri::scope::Scopes>().allow_file(&path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(path.map(|p| p.simplified()))
|
||||
}
|
||||
|
||||
fn message_dialog<R: Runtime>(
|
||||
|
||||
@@ -18,9 +18,6 @@ pub enum Error {
|
||||
#[cfg(mobile)]
|
||||
#[error("Folder picker is not implemented on mobile")]
|
||||
FolderPickerNotImplemented,
|
||||
#[cfg(target_os = "ios")]
|
||||
#[error("File save dialog is not implemented on iOS")]
|
||||
FileSaveDialogNotImplemented,
|
||||
#[error(transparent)]
|
||||
Fs(#[from] tauri_plugin_fs::Error),
|
||||
#[error("URL is not a valid path")]
|
||||
|
||||
Reference in New Issue
Block a user