mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-07-24 17:20:51 +02:00
Co-authored-by: Andrew de Waal <andrewldewaal@gmail.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"dialog": minor
|
||||||
|
"dialog-js": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add `pickerMode` option to file picker (currently only used on iOS)
|
||||||
Generated
+14
@@ -4565,6 +4565,12 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pollster"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "poly1305"
|
name = "poly1305"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -5094,7 +5100,9 @@ dependencies = [
|
|||||||
"objc2-app-kit",
|
"objc2-app-kit",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
"objc2-foundation 0.3.0",
|
"objc2-foundation 0.3.0",
|
||||||
|
"pollster",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
|
"urlencoding",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
@@ -7739,6 +7747,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urlencoding"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urlpattern"
|
name = "urlpattern"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
let filter = null;
|
let filter = null;
|
||||||
let multiple = false;
|
let multiple = false;
|
||||||
let directory = false;
|
let directory = false;
|
||||||
|
let pickerMode = "";
|
||||||
|
|
||||||
function arrayBufferToBase64(buffer, callback) {
|
function arrayBufferToBase64(buffer, callback) {
|
||||||
var blob = new Blob([buffer], {
|
var blob = new Blob([buffer], {
|
||||||
@@ -65,6 +66,7 @@
|
|||||||
: [],
|
: [],
|
||||||
multiple,
|
multiple,
|
||||||
directory,
|
directory,
|
||||||
|
pickerMode: pickerMode === "" ? undefined : pickerMode,
|
||||||
})
|
})
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (Array.isArray(res)) {
|
if (Array.isArray(res)) {
|
||||||
@@ -94,7 +96,7 @@
|
|||||||
onMessage(res);
|
onMessage(res);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(onMessage(res));
|
.catch(onMessage);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(onMessage);
|
.catch(onMessage);
|
||||||
@@ -112,7 +114,7 @@
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
})
|
})
|
||||||
.then(onMessage)
|
.then(onMessage)
|
||||||
.catch(onMessage);
|
.catch(onMessage);
|
||||||
}
|
}
|
||||||
@@ -142,6 +144,16 @@
|
|||||||
<input type="checkbox" id="dialog-directory" bind:checked={directory} />
|
<input type="checkbox" id="dialog-directory" bind:checked={directory} />
|
||||||
<label for="dialog-directory">Directory</label>
|
<label for="dialog-directory">Directory</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="dialog-picker-mode">Picker Mode:</label>
|
||||||
|
<select id="dialog-picker-mode" bind:value={pickerMode}>
|
||||||
|
<option value="">None</option>
|
||||||
|
<option value="media">Media</option>
|
||||||
|
<option value="image">Image</option>
|
||||||
|
<option value="video">Video</option>
|
||||||
|
<option value="document">Document</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<div class="flex flex-wrap flex-col md:flex-row gap-2 children:flex-shrink-0">
|
<div class="flex flex-wrap flex-col md:flex-row gap-2 children:flex-shrink-0">
|
||||||
@@ -156,4 +168,4 @@
|
|||||||
<button class="btn" id="message-dialog" on:click={msg}>Message</button>
|
<button class="btn" id="message-dialog" on:click={msg}>Message</button>
|
||||||
<button class="btn" id="message-dialog" on:click={msgCustom}>Message (custom)</button>
|
<button class="btn" id="message-dialog" on:click={msgCustom}>Message (custom)</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class Filter {
|
|||||||
class FilePickerOptions {
|
class FilePickerOptions {
|
||||||
lateinit var filters: Array<Filter>
|
lateinit var filters: Array<Filter>
|
||||||
var multiple: Boolean? = null
|
var multiple: Boolean? = null
|
||||||
|
var pickerMode: String? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@InvokeArg
|
@InvokeArg
|
||||||
@@ -61,10 +62,19 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) {
|
|||||||
// TODO: ACTION_OPEN_DOCUMENT ??
|
// TODO: ACTION_OPEN_DOCUMENT ??
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
intent.type = "*/*"
|
|
||||||
|
|
||||||
if (parsedTypes.isNotEmpty()) {
|
if (args.pickerMode == "image") {
|
||||||
|
intent.type = "image/*"
|
||||||
|
} else if (args.pickerMode == "video") {
|
||||||
|
intent.type = "video/*"
|
||||||
|
} else if (args.pickerMode == "media") {
|
||||||
|
intent.type = "*/*"
|
||||||
|
intent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("video/*", "image/*"))
|
||||||
|
} else if (parsedTypes.isNotEmpty()) {
|
||||||
|
intent.type = "*/*"
|
||||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, parsedTypes)
|
intent.putExtra(Intent.EXTRA_MIME_TYPES, parsedTypes)
|
||||||
|
} else {
|
||||||
|
intent.type = "*/*"
|
||||||
}
|
}
|
||||||
|
|
||||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, args.multiple ?: false)
|
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, args.multiple ?: false)
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ interface DialogFilter {
|
|||||||
name: string
|
name: string
|
||||||
/**
|
/**
|
||||||
* Extensions to filter, without a `.` prefix.
|
* Extensions to filter, without a `.` prefix.
|
||||||
|
*
|
||||||
|
* **Note:** Mobile platforms have different APIs for filtering that may not support extensions.
|
||||||
|
* iOS: Extensions are supported in the document picker, but not in the media picker.
|
||||||
|
* Android: Extensions are not supported.
|
||||||
|
*
|
||||||
|
* For these platforms, MIME types are the primary way to filter files, as opposed to extensions.
|
||||||
|
* This means the string values here labeled as `extensions` may also be a MIME type.
|
||||||
|
* This property name of `extensions` is being kept for backwards compatibility, but this may be revisited to
|
||||||
|
* specify the difference between extension or MIME type filtering.
|
||||||
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* extensions: ['svg', 'png']
|
* extensions: ['svg', 'png']
|
||||||
@@ -30,7 +40,14 @@ interface DialogFilter {
|
|||||||
interface OpenDialogOptions {
|
interface OpenDialogOptions {
|
||||||
/** The title of the dialog window (desktop only). */
|
/** The title of the dialog window (desktop only). */
|
||||||
title?: string
|
title?: string
|
||||||
/** The filters of the dialog. */
|
/**
|
||||||
|
* The filters of the dialog.
|
||||||
|
* On mobile platforms, if either:
|
||||||
|
* A) the {@linkcode pickerMode} is set to `media`, `image`, or `video`
|
||||||
|
* -- or --
|
||||||
|
* B) the filters include **only** either image or video mime types, the media picker will be displayed.
|
||||||
|
* Otherwise, the document picker will be displayed.
|
||||||
|
*/
|
||||||
filters?: DialogFilter[]
|
filters?: DialogFilter[]
|
||||||
/**
|
/**
|
||||||
* Initial directory or file path.
|
* Initial directory or file path.
|
||||||
@@ -52,6 +69,13 @@ interface OpenDialogOptions {
|
|||||||
recursive?: boolean
|
recursive?: boolean
|
||||||
/** Whether to allow creating directories in the dialog. Enabled by default. **macOS Only** */
|
/** Whether to allow creating directories in the dialog. Enabled by default. **macOS Only** */
|
||||||
canCreateDirectories?: boolean
|
canCreateDirectories?: boolean
|
||||||
|
/**
|
||||||
|
* The preferred mode of the dialog.
|
||||||
|
* This is meant for mobile platforms (iOS and Android) which have distinct file and media pickers.
|
||||||
|
* If not provided, the dialog will automatically choose the best mode based on the MIME types or extensions of the {@linkcode filters}.
|
||||||
|
* On desktop, this option is ignored.
|
||||||
|
*/
|
||||||
|
pickerMode?: PickerMode
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,6 +101,16 @@ interface SaveDialogOptions {
|
|||||||
canCreateDirectories?: boolean
|
canCreateDirectories?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The preferred mode of the dialog.
|
||||||
|
* This is meant for mobile platforms (iOS and Android) which have distinct file and media pickers.
|
||||||
|
* On desktop, this option is ignored.
|
||||||
|
* If not provided, the dialog will automatically choose the best mode based on the MIME types or extensions of the {@linkcode filters}.
|
||||||
|
*
|
||||||
|
* **Note:** This option is only supported on iOS 14 and above. This parameter is ignored on iOS 13 and below.
|
||||||
|
*/
|
||||||
|
export type PickerMode = 'document' | 'media' | 'image' | 'video'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default buttons for a message dialog.
|
* Default buttons for a message dialog.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import PhotosUI
|
|||||||
import SwiftRs
|
import SwiftRs
|
||||||
import Tauri
|
import Tauri
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import UniformTypeIdentifiers
|
||||||
import WebKit
|
import WebKit
|
||||||
|
|
||||||
enum FilePickerEvent {
|
enum FilePickerEvent {
|
||||||
@@ -32,6 +33,7 @@ struct FilePickerOptions: Decodable {
|
|||||||
var multiple: Bool?
|
var multiple: Bool?
|
||||||
var filters: [Filter]?
|
var filters: [Filter]?
|
||||||
var defaultPath: String?
|
var defaultPath: String?
|
||||||
|
var pickerMode: PickerMode?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SaveFileDialogOptions: Decodable {
|
struct SaveFileDialogOptions: Decodable {
|
||||||
@@ -39,6 +41,13 @@ struct SaveFileDialogOptions: Decodable {
|
|||||||
var defaultPath: String?
|
var defaultPath: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PickerMode: String, Decodable {
|
||||||
|
case document
|
||||||
|
case media
|
||||||
|
case image
|
||||||
|
case video
|
||||||
|
}
|
||||||
|
|
||||||
class DialogPlugin: Plugin {
|
class DialogPlugin: Plugin {
|
||||||
|
|
||||||
var filePickerController: FilePickerController!
|
var filePickerController: FilePickerController!
|
||||||
@@ -52,26 +61,6 @@ class DialogPlugin: Plugin {
|
|||||||
@objc public func showFilePicker(_ invoke: Invoke) throws {
|
@objc public func showFilePicker(_ invoke: Invoke) throws {
|
||||||
let args = try invoke.parseArgs(FilePickerOptions.self)
|
let args = try invoke.parseArgs(FilePickerOptions.self)
|
||||||
|
|
||||||
let parsedTypes = parseFiltersOption(args.filters ?? [])
|
|
||||||
|
|
||||||
var isMedia = !parsedTypes.isEmpty
|
|
||||||
var uniqueMimeType: Bool? = nil
|
|
||||||
var mimeKind: String? = nil
|
|
||||||
if !parsedTypes.isEmpty {
|
|
||||||
uniqueMimeType = true
|
|
||||||
for mime in parsedTypes {
|
|
||||||
let kind = mime.components(separatedBy: "/")[0]
|
|
||||||
if kind != "image" && kind != "video" {
|
|
||||||
isMedia = false
|
|
||||||
}
|
|
||||||
if mimeKind == nil {
|
|
||||||
mimeKind = kind
|
|
||||||
} else if mimeKind != kind {
|
|
||||||
uniqueMimeType = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onFilePickerResult = { (event: FilePickerEvent) -> Void in
|
onFilePickerResult = { (event: FilePickerEvent) -> Void in
|
||||||
switch event {
|
switch event {
|
||||||
case .selected(let urls):
|
case .selected(let urls):
|
||||||
@@ -81,51 +70,57 @@ class DialogPlugin: Plugin {
|
|||||||
case .error(let error):
|
case .error(let error):
|
||||||
invoke.reject(error)
|
invoke.reject(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if uniqueMimeType == true || isMedia {
|
if #available(iOS 14, *) {
|
||||||
DispatchQueue.main.async {
|
let parsedTypes = parseFiltersOption(args.filters ?? [])
|
||||||
if #available(iOS 14, *) {
|
|
||||||
|
let mimeKinds = Set(parsedTypes.compactMap { $0.preferredMIMEType?.components(separatedBy: "/")[0] })
|
||||||
|
let filtersIncludeImage = mimeKinds.contains("image")
|
||||||
|
let filtersIncludeVideo = mimeKinds.contains("video")
|
||||||
|
let filtersIncludeNonMedia = mimeKinds.contains(where: { $0 != "image" && $0 != "video" })
|
||||||
|
|
||||||
|
// If the picker mode is media, images, or videos, we always want to show the media picker regardless of what's in the filters.
|
||||||
|
// Otherwise, if the filters A) do not include non-media types and B) include either image or video, we want to show the media picker.
|
||||||
|
if args.pickerMode == .media
|
||||||
|
|| args.pickerMode == .image
|
||||||
|
|| args.pickerMode == .video
|
||||||
|
|| (!filtersIncludeNonMedia && (filtersIncludeImage || filtersIncludeVideo)) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
|
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
|
||||||
configuration.selectionLimit = (args.multiple ?? false) ? 0 : 1
|
configuration.selectionLimit = (args.multiple ?? false) ? 0 : 1
|
||||||
|
|
||||||
if uniqueMimeType == true {
|
// If the filters include image or video, use the appropriate filter.
|
||||||
if mimeKind == "image" {
|
// If both are true, don't define a filter, which means we will display all media.
|
||||||
configuration.filter = .images
|
if args.pickerMode == .image || (filtersIncludeImage && !filtersIncludeVideo) {
|
||||||
} else if mimeKind == "video" {
|
configuration.filter = .images
|
||||||
configuration.filter = .videos
|
} else if args.pickerMode == .video || (filtersIncludeVideo && !filtersIncludeImage) {
|
||||||
}
|
configuration.filter = .videos
|
||||||
}
|
}
|
||||||
|
|
||||||
let picker = PHPickerViewController(configuration: configuration)
|
let picker = PHPickerViewController(configuration: configuration)
|
||||||
picker.delegate = self.filePickerController
|
picker.delegate = self.filePickerController
|
||||||
picker.modalPresentationStyle = .fullScreen
|
picker.modalPresentationStyle = .fullScreen
|
||||||
self.presentViewController(picker)
|
self.presentViewController(picker)
|
||||||
} else {
|
}
|
||||||
let picker = UIImagePickerController()
|
} else {
|
||||||
picker.delegate = self.filePickerController
|
DispatchQueue.main.async {
|
||||||
|
// The UTType.item is the catch-all, allowing for any file type to be selected.
|
||||||
if uniqueMimeType == true && mimeKind == "image" {
|
let contentTypes = parsedTypes.isEmpty ? [UTType.item] : parsedTypes
|
||||||
picker.sourceType = .photoLibrary
|
let picker: UIDocumentPickerViewController = UIDocumentPickerViewController(forOpeningContentTypes: contentTypes, asCopy: true)
|
||||||
|
|
||||||
|
if let defaultPath = args.defaultPath {
|
||||||
|
picker.directoryURL = URL(string: defaultPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
picker.sourceType = .photoLibrary
|
picker.delegate = self.filePickerController
|
||||||
|
picker.allowsMultipleSelection = args.multiple ?? false
|
||||||
picker.modalPresentationStyle = .fullScreen
|
picker.modalPresentationStyle = .fullScreen
|
||||||
self.presentViewController(picker)
|
self.presentViewController(picker)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let documentTypes = parsedTypes.isEmpty ? ["public.data"] : parsedTypes
|
showFilePickerLegacy(args: args)
|
||||||
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
|
|
||||||
self.presentViewController(picker)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,19 +168,80 @@ class DialogPlugin: Plugin {
|
|||||||
self.manager.viewController?.present(viewControllerToPresent, animated: true, completion: nil)
|
self.manager.viewController?.present(viewControllerToPresent, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func parseFiltersOption(_ filters: [Filter]) -> [String] {
|
@available(iOS 14, *)
|
||||||
|
private func parseFiltersOption(_ filters: [Filter]) -> [UTType] {
|
||||||
|
var parsedTypes: [UTType] = []
|
||||||
|
for filter in filters {
|
||||||
|
for ext in filter.extensions ?? [] {
|
||||||
|
// We need to support extensions as well as MIME types.
|
||||||
|
if let utType = UTType(mimeType: ext) {
|
||||||
|
parsedTypes.append(utType)
|
||||||
|
} else if let utType = UTType(filenameExtension: ext) {
|
||||||
|
parsedTypes.append(utType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedTypes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function is only used for iOS < 14, and should be removed if/when the deployment target is raised to 14.
|
||||||
|
private func showFilePickerLegacy(args: FilePickerOptions) {
|
||||||
|
let parsedTypes = parseFiltersOptionLegacy(args.filters ?? [])
|
||||||
|
|
||||||
|
var filtersIncludeImage: Bool = false
|
||||||
|
var filtersIncludeVideo: Bool = false
|
||||||
|
var filtersIncludeNonMedia: Bool = false
|
||||||
|
|
||||||
|
if !parsedTypes.isEmpty {
|
||||||
|
let mimeKinds = Set(parsedTypes.map { $0.components(separatedBy: "/")[0] })
|
||||||
|
filtersIncludeImage = mimeKinds.contains("image")
|
||||||
|
filtersIncludeVideo = mimeKinds.contains("video")
|
||||||
|
filtersIncludeNonMedia = mimeKinds.contains(where: { $0 != "image" && $0 != "video" })
|
||||||
|
}
|
||||||
|
|
||||||
|
if !filtersIncludeNonMedia && (filtersIncludeImage || filtersIncludeVideo) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let picker = UIImagePickerController()
|
||||||
|
picker.delegate = self.filePickerController
|
||||||
|
|
||||||
|
if filtersIncludeImage && !filtersIncludeVideo {
|
||||||
|
picker.sourceType = .photoLibrary
|
||||||
|
}
|
||||||
|
|
||||||
|
picker.modalPresentationStyle = .fullScreen
|
||||||
|
self.presentViewController(picker)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
self.presentViewController(picker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function is only used for iOS < 14, and should be removed if/when the deployment target is raised to 14.
|
||||||
|
private func parseFiltersOptionLegacy(_ filters: [Filter]) -> [String] {
|
||||||
var parsedTypes: [String] = []
|
var parsedTypes: [String] = []
|
||||||
for filter in filters {
|
for filter in filters {
|
||||||
for ext in filter.extensions ?? [] {
|
for ext in filter.extensions ?? [] {
|
||||||
guard
|
guard
|
||||||
let utType: String = UTTypeCreatePreferredIdentifierForTag(
|
let utType: String = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, ext as CFString, nil)?.takeRetainedValue() as String?
|
||||||
kUTTagClassMIMEType, ext as CFString, nil)?.takeRetainedValue() as String?
|
|
||||||
else {
|
else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
parsedTypes.append(utType)
|
parsedTypes.append(utType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsedTypes
|
return parsedTypes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use tauri_plugin_fs::FsExt;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Dialog, FileDialogBuilder, FilePath, MessageDialogBuilder, MessageDialogButtons,
|
Dialog, FileDialogBuilder, FilePath, MessageDialogBuilder, MessageDialogButtons,
|
||||||
MessageDialogKind, MessageDialogResult, Result, CANCEL, NO, OK, YES,
|
MessageDialogKind, MessageDialogResult, PickerMode, Result, CANCEL, NO, OK, YES,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -56,6 +56,13 @@ pub struct OpenDialogOptions {
|
|||||||
recursive: bool,
|
recursive: bool,
|
||||||
/// Whether to allow creating directories in the dialog **macOS Only**
|
/// Whether to allow creating directories in the dialog **macOS Only**
|
||||||
can_create_directories: Option<bool>,
|
can_create_directories: Option<bool>,
|
||||||
|
/// The preferred mode of the dialog.
|
||||||
|
/// This is meant for mobile platforms (iOS and Android) which have distinct file and media pickers.
|
||||||
|
/// On desktop, this option is ignored.
|
||||||
|
/// If not provided, the dialog will automatically choose the best mode based on the MIME types of the filters.
|
||||||
|
#[serde(default)]
|
||||||
|
#[cfg_attr(mobile, allow(dead_code))]
|
||||||
|
picker_mode: Option<PickerMode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The options for the save dialog API.
|
/// The options for the save dialog API.
|
||||||
@@ -127,6 +134,9 @@ pub(crate) async fn open<R: Runtime>(
|
|||||||
if let Some(can) = options.can_create_directories {
|
if let Some(can) = options.can_create_directories {
|
||||||
dialog_builder = dialog_builder.set_can_create_directories(can);
|
dialog_builder = dialog_builder.set_can_create_directories(can);
|
||||||
}
|
}
|
||||||
|
if let Some(picker_mode) = options.picker_mode {
|
||||||
|
dialog_builder = dialog_builder.set_picker_mode(picker_mode);
|
||||||
|
}
|
||||||
for filter in options.filters {
|
for filter in options.filters {
|
||||||
let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect();
|
let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect();
|
||||||
dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
|
dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
|
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::{
|
use tauri::{
|
||||||
plugin::{Builder, TauriPlugin},
|
plugin::{Builder, TauriPlugin},
|
||||||
Manager, Runtime,
|
Manager, Runtime,
|
||||||
@@ -44,6 +44,15 @@ pub use desktop::Dialog;
|
|||||||
#[cfg(mobile)]
|
#[cfg(mobile)]
|
||||||
pub use mobile::Dialog;
|
pub use mobile::Dialog;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum PickerMode {
|
||||||
|
Document,
|
||||||
|
Media,
|
||||||
|
Image,
|
||||||
|
Video,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) const OK: &str = "Ok";
|
pub(crate) const OK: &str = "Ok";
|
||||||
pub(crate) const CANCEL: &str = "Cancel";
|
pub(crate) const CANCEL: &str = "Cancel";
|
||||||
pub(crate) const YES: &str = "Yes";
|
pub(crate) const YES: &str = "Yes";
|
||||||
@@ -369,6 +378,7 @@ pub struct FileDialogBuilder<R: Runtime> {
|
|||||||
pub(crate) file_name: Option<String>,
|
pub(crate) file_name: Option<String>,
|
||||||
pub(crate) title: Option<String>,
|
pub(crate) title: Option<String>,
|
||||||
pub(crate) can_create_directories: Option<bool>,
|
pub(crate) can_create_directories: Option<bool>,
|
||||||
|
pub(crate) picker_mode: Option<PickerMode>,
|
||||||
#[cfg(desktop)]
|
#[cfg(desktop)]
|
||||||
pub(crate) parent: Option<crate::desktop::WindowHandle>,
|
pub(crate) parent: Option<crate::desktop::WindowHandle>,
|
||||||
}
|
}
|
||||||
@@ -380,6 +390,7 @@ pub(crate) struct FileDialogPayload<'a> {
|
|||||||
file_name: &'a Option<String>,
|
file_name: &'a Option<String>,
|
||||||
filters: &'a Vec<Filter>,
|
filters: &'a Vec<Filter>,
|
||||||
multiple: bool,
|
multiple: bool,
|
||||||
|
picker_mode: &'a Option<PickerMode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// raw window handle :(
|
// raw window handle :(
|
||||||
@@ -395,6 +406,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
|
|||||||
file_name: None,
|
file_name: None,
|
||||||
title: None,
|
title: None,
|
||||||
can_create_directories: None,
|
can_create_directories: None,
|
||||||
|
picker_mode: None,
|
||||||
#[cfg(desktop)]
|
#[cfg(desktop)]
|
||||||
parent: None,
|
parent: None,
|
||||||
}
|
}
|
||||||
@@ -406,6 +418,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
|
|||||||
file_name: &self.file_name,
|
file_name: &self.file_name,
|
||||||
filters: &self.filters,
|
filters: &self.filters,
|
||||||
multiple,
|
multiple,
|
||||||
|
picker_mode: &self.picker_mode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,6 +479,15 @@ impl<R: Runtime> FileDialogBuilder<R> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the picker mode of the dialog.
|
||||||
|
/// This is meant for mobile platforms (iOS and Android) which have distinct file and media pickers.
|
||||||
|
/// On desktop, this option is ignored.
|
||||||
|
/// If not provided, the dialog will automatically choose the best mode based on the MIME types of the filters.
|
||||||
|
pub fn set_picker_mode(mut self, mode: PickerMode) -> Self {
|
||||||
|
self.picker_mode.replace(mode);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Shows the dialog to select a single file.
|
/// Shows the dialog to select a single file.
|
||||||
///
|
///
|
||||||
/// This is not a blocking operation,
|
/// This is not a blocking operation,
|
||||||
|
|||||||
Reference in New Issue
Block a user