feat: add android file picker

pull/306/head
Lucas Nogueira 2 years ago
parent bf03316a04
commit 778c99aa0c
No known key found for this signature in database
GPG Key ID: FFEA6C72E73482F1

18
Cargo.lock generated

@ -4344,7 +4344,7 @@ dependencies = [
[[package]]
name = "tauri"
version = "2.0.0-alpha.6"
version = "2.0.0-alpha.7"
dependencies = [
"anyhow",
"bytes 1.4.0",
@ -4394,7 +4394,7 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "2.0.0-alpha.3"
version = "2.0.0-alpha.4"
dependencies = [
"anyhow",
"cargo_toml",
@ -4412,7 +4412,7 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "2.0.0-alpha.3"
version = "2.0.0-alpha.4"
dependencies = [
"base64 0.21.0",
"brotli",
@ -4436,7 +4436,7 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "2.0.0-alpha.3"
version = "2.0.0-alpha.4"
dependencies = [
"heck",
"proc-macro2",
@ -4671,7 +4671,7 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "0.13.0-alpha.3"
version = "0.13.0-alpha.4"
dependencies = [
"gtk",
"http",
@ -4691,7 +4691,7 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "0.13.0-alpha.3"
version = "0.13.0-alpha.4"
dependencies = [
"cocoa",
"gtk",
@ -4710,7 +4710,7 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "2.0.0-alpha.3"
version = "2.0.0-alpha.4"
dependencies = [
"brotli",
"ctor",
@ -5813,9 +5813,9 @@ dependencies = [
[[package]]
name = "wry"
version = "0.27.2"
version = "0.27.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a04fc11ebe79d2ed88670a72f0bee21e35773b893993bfa577bd448af8ff2e5"
checksum = "e8cf0dbfa7ccbd2e3832a3098b19d4b552360ea00a40b244a99caef46bffd84f"
dependencies = [
"base64 0.13.1",
"block",

@ -6,16 +6,149 @@ package app.tauri.dialog
import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.net.Uri
import android.os.Handler
import android.os.Looper
import androidx.activity.result.ActivityResult
import app.tauri.Logger
import app.tauri.annotation.ActivityCallback
import app.tauri.annotation.Command
import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.Invoke
import app.tauri.plugin.JSArray
import app.tauri.plugin.JSObject
import app.tauri.plugin.Plugin
import org.json.JSONException
@TauriPlugin
class DialogPlugin(private val activity: Activity): Plugin(activity) {
@Command
fun showFilePicker(invoke: Invoke) {
try {
val filters = invoke.getArray("filters", JSArray())
val multiple = invoke.getBoolean("multiple", false)
val parsedTypes = parseFiltersOption(filters)
val intent = if (parsedTypes != null && parsedTypes.isNotEmpty()) {
val intent = Intent(Intent.ACTION_PICK)
intent.putExtra(Intent.EXTRA_MIME_TYPES, parsedTypes)
var uniqueMimeType = true
var mimeKind: String? = null
for (mime in parsedTypes) {
val kind = mime.split("/")[0]
if (mimeKind == null) {
mimeKind = kind
} else if (mimeKind != kind) {
uniqueMimeType = false
}
}
intent.type = if (uniqueMimeType) Intent.normalizeMimeType("$mimeKind/*") else "*/*"
intent
} else {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*"
intent
}
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple)
startActivityForResult(invoke, intent, "filePickerResult")
} catch (ex: Exception) {
val message = ex.message ?: "Failed to pick file"
Logger.error(message)
invoke.reject(message)
}
}
@ActivityCallback
fun filePickerResult(invoke: Invoke, result: ActivityResult) {
try {
val readData = invoke.getBoolean("readData", false)
when (result.resultCode) {
Activity.RESULT_OK -> {
val callResult = createPickFilesResult(result.data, readData)
invoke.resolve(callResult)
}
Activity.RESULT_CANCELED -> invoke.reject("File picker cancelled")
else -> invoke.reject("Failed to pick files")
}
} catch (ex: java.lang.Exception) {
val message = ex.message ?: "Failed to read file pick result"
Logger.error(message)
invoke.reject(message)
}
}
private fun createPickFilesResult(data: Intent?, readData: Boolean): JSObject {
val callResult = JSObject()
val filesResultList: MutableList<JSObject> = ArrayList()
if (data == null) {
callResult.put("files", JSArray.from(filesResultList))
return callResult
}
val uris: MutableList<Uri?> = ArrayList()
if (data.clipData == null) {
val uri: Uri? = data.data
uris.add(uri)
} else {
for (i in 0 until data.clipData!!.itemCount) {
val uri: Uri = data.clipData!!.getItemAt(i).uri
uris.add(uri)
}
}
for (i in uris.indices) {
val uri = uris[i] ?: continue
val fileResult = JSObject()
if (readData) {
fileResult.put("base64Data", FilePickerUtils.getDataFromUri(activity, uri))
}
val duration = FilePickerUtils.getDurationFromUri(activity, uri)
if (duration != null) {
fileResult.put("duration", duration)
}
val resolution = FilePickerUtils.getHeightAndWidthFromUri(activity, uri)
if (resolution != null) {
fileResult.put("height", resolution.height)
fileResult.put("width", resolution.width)
}
fileResult.put("mimeType", FilePickerUtils.getMimeTypeFromUri(activity, uri))
val modifiedAt = FilePickerUtils.getModifiedAtFromUri(activity, uri)
if (modifiedAt != null) {
fileResult.put("modifiedAt", modifiedAt)
}
fileResult.put("name", FilePickerUtils.getNameFromUri(activity, uri))
fileResult.put("path", FilePickerUtils.getPathFromUri(uri))
fileResult.put("size", FilePickerUtils.getSizeFromUri(activity, uri))
filesResultList.add(fileResult)
}
callResult.put("files", JSArray.from(filesResultList.toTypedArray()))
return callResult
}
private fun parseFiltersOption(filters: JSArray): Array<String>? {
return try {
val filtersList: List<JSObject> = filters.toList()
val mimeTypes = mutableListOf<String>()
for (filter in filtersList) {
val extensionsList = filter.getJSONArray("extensions")
for (i in 0 until extensionsList.length()) {
val mime = extensionsList.getString(i)
mimeTypes.add(if (mime == "text/csv") "text/comma-separated-values" else mime)
}
}
mimeTypes.toTypedArray()
} catch (exception: JSONException) {
Logger.error("parseTypesOption failed.", exception)
null
}
}
@Command
fun showMessageDialog(invoke: Invoke) {
val title = invoke.getString("title")

@ -0,0 +1,165 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.dialog
import android.content.Context
import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.provider.DocumentsContract
import android.provider.OpenableColumns
import android.util.Base64
import app.tauri.Logger
import java.io.ByteArrayOutputStream
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
class FilePickerUtils {
class FileResolution(var height: Int, var width: Int)
companion object {
fun getPathFromUri(uri: Uri): String {
return uri.toString()
}
fun getNameFromUri(context: Context, uri: Uri): String? {
var displayName: String? = ""
val projection = arrayOf(OpenableColumns.DISPLAY_NAME)
val cursor =
context.contentResolver.query(uri, projection, null, null, null)
if (cursor != null) {
cursor.moveToFirst()
val columnIdx = cursor.getColumnIndex(projection[0])
displayName = cursor.getString(columnIdx)
cursor.close()
}
if (displayName == null || displayName.isEmpty()) {
displayName = uri.lastPathSegment
}
return displayName
}
fun getDataFromUri(context: Context, uri: Uri): String {
try {
val stream = context.contentResolver.openInputStream(uri) ?: return ""
val bytes = getBytesFromInputStream(stream)
return Base64.encodeToString(bytes, Base64.NO_WRAP)
} catch (e: FileNotFoundException) {
Logger.error("openInputStream failed.", e)
} catch (e: IOException) {
Logger.error("getBytesFromInputStream failed.", e)
}
return ""
}
fun getMimeTypeFromUri(context: Context, uri: Uri): String? {
return context.contentResolver.getType(uri)
}
fun getModifiedAtFromUri(context: Context, uri: Uri): Long? {
return try {
var modifiedAt: Long = 0
val cursor =
context.contentResolver.query(uri, null, null, null, null)
if (cursor != null) {
cursor.moveToFirst()
val columnIdx =
cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED)
modifiedAt = cursor.getLong(columnIdx)
cursor.close()
}
modifiedAt
} catch (e: Exception) {
Logger.error("getModifiedAtFromUri failed.", e)
null
}
}
fun getSizeFromUri(context: Context, uri: Uri): Long {
var size: Long = 0
val projection = arrayOf(OpenableColumns.SIZE)
val cursor =
context.contentResolver.query(uri, projection, null, null, null)
if (cursor != null) {
cursor.moveToFirst()
val columnIdx = cursor.getColumnIndex(projection[0])
size = cursor.getLong(columnIdx)
cursor.close()
}
return size
}
fun getDurationFromUri(context: Context, uri: Uri): Long? {
if (isVideoUri(context, uri)) {
val retriever = MediaMetadataRetriever()
retriever.setDataSource(context, uri)
val time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
val durationMs = time?.toLong() ?: 0
try {
retriever.release()
} catch (e: Exception) {
Logger.error("MediaMetadataRetriever.release() failed.", e)
}
return durationMs / 1000L
}
return null
}
fun getHeightAndWidthFromUri(context: Context, uri: Uri): FileResolution? {
if (isImageUri(context, uri)) {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
return try {
BitmapFactory.decodeStream(
context.contentResolver.openInputStream(uri),
null,
options
)
FileResolution(options.outHeight, options.outWidth)
} catch (exception: FileNotFoundException) {
exception.printStackTrace()
null
}
} else if (isVideoUri(context, uri)) {
val retriever = MediaMetadataRetriever()
retriever.setDataSource(context, uri)
val width =
Integer.valueOf(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) ?: "0")
val height =
Integer.valueOf(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT) ?: "0")
try {
retriever.release()
} catch (e: Exception) {
Logger.error("MediaMetadataRetriever.release() failed.", e)
}
return FileResolution(height, width)
}
return null
}
private fun isImageUri(context: Context, uri: Uri): Boolean {
val mimeType = getMimeTypeFromUri(context, uri) ?: return false
return mimeType.startsWith("image")
}
private fun isVideoUri(context: Context, uri: Uri): Boolean {
val mimeType = getMimeTypeFromUri(context, uri) ?: return false
return mimeType.startsWith("video")
}
@Throws(IOException::class)
private fun getBytesFromInputStream(`is`: InputStream): ByteArray {
val os = ByteArrayOutputStream()
val buffer = ByteArray(0xFFFF)
var len = `is`.read(buffer)
while (len != -1) {
os.write(buffer, 0, len)
len = `is`.read(buffer)
}
return os.toByteArray()
}
}
}

@ -10,12 +10,11 @@
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^1.1.0",
"tauri-plugin-dialog-api": "link:../../"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tauri-apps/cli": "^2.0.0-alpha.6",
"@tauri-apps/cli": "^2.0.0-alpha.7",
"internal-ip": "^7.0.0",
"svelte": "^3.49.0",
"vite": "^3.0.2"

@ -3061,7 +3061,7 @@ dependencies = [
[[package]]
name = "tauri"
version = "2.0.0-alpha.6"
version = "2.0.0-alpha.7"
dependencies = [
"anyhow",
"bytes",
@ -3118,7 +3118,7 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "2.0.0-alpha.3"
version = "2.0.0-alpha.4"
dependencies = [
"anyhow",
"cargo_toml",
@ -3136,7 +3136,7 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "2.0.0-alpha.3"
version = "2.0.0-alpha.4"
dependencies = [
"base64 0.21.0",
"brotli",
@ -3161,7 +3161,7 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "2.0.0-alpha.3"
version = "2.0.0-alpha.4"
dependencies = [
"heck 0.4.1",
"proc-macro2",
@ -3205,7 +3205,7 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "0.13.0-alpha.3"
version = "0.13.0-alpha.4"
dependencies = [
"gtk",
"http",
@ -3225,7 +3225,7 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "0.13.0-alpha.3"
version = "0.13.0-alpha.4"
dependencies = [
"cocoa",
"gtk",
@ -3244,7 +3244,7 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "2.0.0-alpha.3"
version = "2.0.0-alpha.4"
dependencies = [
"brotli",
"ctor",
@ -4057,9 +4057,9 @@ dependencies = [
[[package]]
name = "wry"
version = "0.27.1"
version = "0.27.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b958a84f347bf8ec2e882b3f767bdb3f7797e89867bb9d6f0d1fe3df26754fe9"
checksum = "e8cf0dbfa7ccbd2e3832a3098b19d4b552360ea00a40b244a99caef46bffd84f"
dependencies = [
"base64 0.13.1",
"block",

@ -9,8 +9,6 @@
/.idea/assetWizardSettings.xml
.DS_Store
build
/buildSrc/src/main/java/app/tauri/app/kotlin/BuildTask.kt
/buildSrc/src/main/java/app/tauri/app/kotlin/RustPlugin.kt
/captures
.externalNativeBuild
.cxx

@ -0,0 +1,58 @@
package app.tauri
import java.io.File
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.logging.LogLevel
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
open class BuildTask : DefaultTask() {
@InputDirectory
@PathSensitive(PathSensitivity.RELATIVE)
var rootDirRel: File? = null
@Input
var target: String? = null
@Input
var release: Boolean? = null
@TaskAction
fun build() {
val executable = """yarn""";
try {
runTauriCli(executable)
} catch (e: Exception){
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
runTauriCli("$executable.cmd")
} else {
throw e;
}
}
}
fun runTauriCli(executable: String) {
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
val target = target ?: throw GradleException("target cannot be null")
val release = release ?: throw GradleException("release cannot be null")
val args = listOf("tauri", "android", "android-studio-script");
project.exec {
workingDir(File(project.projectDir, rootDirRel.path))
executable(executable)
args(args)
if (project.logger.isEnabled(LogLevel.DEBUG)) {
args("-vv")
} else if (project.logger.isEnabled(LogLevel.INFO)) {
args("-v")
}
if (release) {
args("--release")
}
args(listOf("--target", target))
}.assertNormalExitValue()
}
}

@ -0,0 +1,59 @@
package app.tauri
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import java.io.File
import java.util.*
const val TASK_GROUP = "rust"
open class Config {
var rootDirRel: String? = null
var targets: List<String>? = null
var arches: List<String>? = null
}
open class RustPlugin : Plugin<Project> {
private lateinit var config: Config
override fun apply(project: Project) {
config = project.extensions.create("rust", Config::class.java)
project.afterEvaluate {
if (config.targets == null) {
throw GradleException("targets cannot be null")
}
if (config.arches == null) {
throw GradleException("arches cannot be null")
}
for (profile in listOf("debug", "release")) {
val profileCapitalized = profile.capitalize(Locale.ROOT)
val buildTask = project.tasks.maybeCreate(
"rustBuild$profileCapitalized",
DefaultTask::class.java
).apply {
group = TASK_GROUP
description = "Build dynamic library in $profile mode for all targets"
}
for (targetPair in config.targets!!.withIndex()) {
val targetName = targetPair.value
val targetArch = config.arches!![targetPair.index]
val targetArchCapitalized = targetArch.capitalize(Locale.ROOT)
val targetBuildTask = project.tasks.maybeCreate(
"rustBuild$targetArchCapitalized$profileCapitalized",
BuildTask::class.java
).apply {
group = TASK_GROUP
description = "Build dynamic library in $profile mode for $targetArch"
rootDirRel = config.rootDirRel?.let { File(it) }
target = targetName
release = profile == "release"
}
buildTask.dependsOn(targetBuildTask)
project.tasks.findByName("preBuild")?.mustRunAfter(targetBuildTask)
}
}
}
}
}

@ -1,9 +1,19 @@
<script>
import MessageDialogs from "./lib/MessageDialogs.svelte";
import FileDialogs from "./lib/FileDialogs.svelte";
</script>
<main class="container">
<div class="row">
<MessageDialogs />
</div>
<div class="row">
<FileDialogs />
</div>
</main>
<style>
.container > .row {
margin-top: 24px;
}
</style>

@ -0,0 +1,27 @@
<script>
import * as dialog from "tauri-plugin-dialog-api";
let response = "";
async function open() {
try {
const res = await dialog.open({
filters: [
{
name: "test",
extensions: ["image/jpg"],
},
],
multiple: false,
});
response = JSON.stringify(res, null, 2);
} catch (e) {
console.error(e);
}
}
</script>
<div>
<div>{response}</div>
<button on:click={open}> Open </button>
</div>

@ -24,70 +24,70 @@
svelte-hmr "^0.15.1"
vitefu "^0.2.2"
"@tauri-apps/api@^1.1.0", "@tauri-apps/api@^1.2.0":
"@tauri-apps/api@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-1.2.0.tgz#1f196b3e012971227f41b98214c846430a4eb477"
integrity sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw==
"@tauri-apps/cli-darwin-arm64@2.0.0-alpha.6":
version "2.0.0-alpha.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-alpha.6.tgz#d4fd5aa715bb3df7513f321636b3bcbade81fe06"
integrity sha512-Oc6EUaXRsAXCapl5EdEqkzMUkCpqQ9ELXhLORwgVTDNYjq11xpe/VxYlVoao/FuEp9DnHvcdZVlgNrEgWb8whQ==
"@tauri-apps/cli-darwin-x64@2.0.0-alpha.6":
version "2.0.0-alpha.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-alpha.6.tgz#55455274f6860f2412f99a1b2603bffcdce691e7"
integrity sha512-HhF4XFTsznCzAVXQJd1e+yi1DdVD5PHOXVl2ZjeqmALvci4X/yCZEj4sCg4+Qukvs8BVnDYRvGs7M8cjbJZJoQ==
"@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-alpha.6":
version "2.0.0-alpha.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-alpha.6.tgz#88bce2a4714530447d43a0e8bf3bebbc6162980b"
integrity sha512-diHDgkBtx1exs/vlouy0B0MrUkJL0ojCJZcoq3rR76kMk2Gmsxz43AsoQfgziE9BCS2luIwilMuofgKOY4s+Zg==
"@tauri-apps/cli-linux-arm64-gnu@2.0.0-alpha.6":
version "2.0.0-alpha.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-alpha.6.tgz#388bfa33cf7dc4ce6676f3be7ebe34fdbab7dfa2"
integrity sha512-g8SpZ3rVUdVIfuuosdnq3U359KF48R374b1G3xUUyebJAwWZ5Bay0P94RW0JNoqbM9Z4UpSedrCgIPcunnmZHw==
"@tauri-apps/cli-linux-arm64-musl@2.0.0-alpha.6":
version "2.0.0-alpha.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-alpha.6.tgz#90059465acfd397aeba0d6a12d2bb6fbd6f0fc06"
integrity sha512-RD5u0MoBKruMZK7jp4tcQ9JUU6wx25+m0HfT+WSC3YGV8waRxVT/yxyxH5FEPhsxsT0LUQohOUF2S3oB4/bYIg==
"@tauri-apps/cli-linux-x64-gnu@2.0.0-alpha.6":
version "2.0.0-alpha.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-alpha.6.tgz#6d0e6e371168ee38c0657e519f227d01e8e9f181"
integrity sha512-AWJbXJ8bZgwWDorHnCLdLKdz7j9sQqnhKaP+GTNZ2bBUuM03rVYfB5GAOmteqSAXOWfzT/LCLIZIQaZ+Y36vHg==
"@tauri-apps/cli-linux-x64-musl@2.0.0-alpha.6":
version "2.0.0-alpha.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-alpha.6.tgz#ad4d978c8babaea424b502e901bf1be20eea2241"
integrity sha512-0+FoSopNCHdwEWmdYdlY+G8WXa2ojFXAqs1vIiJlQ4Z6e1Lv6dmPww+FmhNChW4bSi/HH1PbdPX0FB0WEFnM2g==
"@tauri-apps/cli-win32-ia32-msvc@2.0.0-alpha.6":
version "2.0.0-alpha.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-alpha.6.tgz#5db864a62beecfa446db0e9af6afc152d8bdec32"
integrity sha512-JsdlWyP2szDvOXhNMSbDUMxb0t5ppl80AAtUjtNM47nyM2/QtoMYPZ6eIjxmKnss/ZbRCF5eCEgP1v5/x+W/gw==
"@tauri-apps/cli-win32-x64-msvc@2.0.0-alpha.6":
version "2.0.0-alpha.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-alpha.6.tgz#7404db576ee00bbbf99399905b9fedaec48159f2"
integrity sha512-RY4aCCIyiwuD0iDxzwvqDSyoMoz6xgoGzWS4Xy2wKvoi/ptUAPRn8uhI3JTLFH4U+qky0KXO1oonBfOTF/pWQw==
"@tauri-apps/cli@^2.0.0-alpha.6":
version "2.0.0-alpha.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-2.0.0-alpha.6.tgz#bcfef07161f9cf695485488f54559ae02f61c992"
integrity sha512-5apIpCGSuf5fUKYW1Jw0Qi1AAMvlUfr7jX41lhIrpXYeOD+Q8HKlpgpRpUScONfA9TThJBUuOfSVPbEkYMsyfw==
"@tauri-apps/cli-darwin-arm64@2.0.0-alpha.7":
version "2.0.0-alpha.7"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-alpha.7.tgz#976c4b24db7efced6c9b225f4c767f88fad3a375"
integrity sha512-Mv4GGMBUU8c31bZhQKRs00Yo39bj0kAfJHPiEld+oBGy/qs9PskM1f4GXjaBB0LTuzdgRfzgWLHR+EDP/2LYAQ==
"@tauri-apps/cli-darwin-x64@2.0.0-alpha.7":
version "2.0.0-alpha.7"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-alpha.7.tgz#08aab5785c1591ea4a62d38c43099bce53c0d9fc"
integrity sha512-ucKCWJrALVx9gfWpKPF+oLdgHqk0HrbnNi/q8EtUcRoZ3lRRgKIMiiqD9tzjgx80IkFtXni3drBmBlP1J4sWiw==
"@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-alpha.7":
version "2.0.0-alpha.7"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-alpha.7.tgz#5970b5fd20efede79a685f09eb4c0c9427a11c46"
integrity sha512-U9Cv6/SN8IDRLaRIBYPKTIBKuRSwMhwsAeGqevz6JU2/Br9B6L6tiyurDBpsfUo67B0XdqX0/mJ5r3TlKuQknw==
"@tauri-apps/cli-linux-arm64-gnu@2.0.0-alpha.7":
version "2.0.0-alpha.7"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-alpha.7.tgz#52ff0a852028643ade289a5a9a6e05207d5f60e2"
integrity sha512-rFDUif4DuRWkz9KkJGTekpY6k4FrJdRFrfcxHhaw16q1Q2V/+yIUnAxEDGcJNny+BnDbvsurFCrKOiIdC7BDZw==
"@tauri-apps/cli-linux-arm64-musl@2.0.0-alpha.7":
version "2.0.0-alpha.7"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-alpha.7.tgz#ccdbe600044099b3c355947a599bffc0ef8001d2"
integrity sha512-0SObooHyStCtAtvGsmQm7dHeYWJgD+JECkl5hLRBp2sPIY+0iuyNcibhwTZzkwgOUzNlyY1usgRXYe7bmFunLA==
"@tauri-apps/cli-linux-x64-gnu@2.0.0-alpha.7":
version "2.0.0-alpha.7"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-alpha.7.tgz#12a2894b07df723c7aaccd2f59b81cb76d29ebd9"
integrity sha512-nE4iT5tDp+sTyJxxuDvnAdiUtKRGel9QCZsAxUYbqixagogxqf9RZWg4vtcyspxDpynOIMwe0EONj/pAOXNmsQ==
"@tauri-apps/cli-linux-x64-musl@2.0.0-alpha.7":
version "2.0.0-alpha.7"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-alpha.7.tgz#29ab9300b8c6ab68e3d2db8a8ce9ff4ba57b1e9a"
integrity sha512-78WoMrOjGUWmSgy2wRLrYov2mkvxjWjwTSre6Hy+gBUCVHfflxE9Lp8a+oEjkGZCO0oDVrLqAhZrxA4jaDFCKw==
"@tauri-apps/cli-win32-ia32-msvc@2.0.0-alpha.7":
version "2.0.0-alpha.7"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-alpha.7.tgz#ce09752e5feaa1a69f6c5a0fb77b7b7d1c763a37"
integrity sha512-GNKzxb+3MvQd1pH5r42aGHQtv7RoGT2mnBUm4AnpvKI8qHTmyk/bza7X68zoHjtK+qRoESI2g1mtCq/DvyW43w==
"@tauri-apps/cli-win32-x64-msvc@2.0.0-alpha.7":
version "2.0.0-alpha.7"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-alpha.7.tgz#c2790d6389c3f869e33689f3a630d76f7857bbe5"
integrity sha512-i4cpzbtb5sqIiv/+kpJ2nF6NO0RnG1kp9DIivMQjTIBsNa+lPL+q5f0vK5TJwUZArhmFM7RmoVmRYB3lq+T23w==
"@tauri-apps/cli@^2.0.0-alpha.7":
version "2.0.0-alpha.7"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-2.0.0-alpha.7.tgz#0efa2c0d6be203da41f20d12a19ecc20d68fdcb6"
integrity sha512-BCIgUd/4FZeZzDXb+V0Bp+OuXtHmuxyRVEkA3i509e9+MSOKndUmtppn/5XmvxrJxf0GV7kXRTmVaDzl6w7O7w==
optionalDependencies:
"@tauri-apps/cli-darwin-arm64" "2.0.0-alpha.6"
"@tauri-apps/cli-darwin-x64" "2.0.0-alpha.6"
"@tauri-apps/cli-linux-arm-gnueabihf" "2.0.0-alpha.6"
"@tauri-apps/cli-linux-arm64-gnu" "2.0.0-alpha.6"
"@tauri-apps/cli-linux-arm64-musl" "2.0.0-alpha.6"
"@tauri-apps/cli-linux-x64-gnu" "2.0.0-alpha.6"
"@tauri-apps/cli-linux-x64-musl" "2.0.0-alpha.6"
"@tauri-apps/cli-win32-ia32-msvc" "2.0.0-alpha.6"
"@tauri-apps/cli-win32-x64-msvc" "2.0.0-alpha.6"
"@tauri-apps/cli-darwin-arm64" "2.0.0-alpha.7"
"@tauri-apps/cli-darwin-x64" "2.0.0-alpha.7"
"@tauri-apps/cli-linux-arm-gnueabihf" "2.0.0-alpha.7"
"@tauri-apps/cli-linux-arm64-gnu" "2.0.0-alpha.7"
"@tauri-apps/cli-linux-arm64-musl" "2.0.0-alpha.7"
"@tauri-apps/cli-linux-x64-gnu" "2.0.0-alpha.7"
"@tauri-apps/cli-linux-x64-musl" "2.0.0-alpha.7"
"@tauri-apps/cli-win32-ia32-msvc" "2.0.0-alpha.7"
"@tauri-apps/cli-win32-x64-msvc" "2.0.0-alpha.7"
cross-spawn@^7.0.3:
version "7.0.3"

@ -4,6 +4,18 @@
import { invoke } from '@tauri-apps/api/tauri'
interface FileResponse {
base64Data?: string
duration?: number
height?: number
width?: number
mimeType?: string
modifiedAt?: number
name?: string
path: string
size: number
}
/**
* Extension filters for the file dialog.
*
@ -86,6 +98,18 @@ interface ConfirmDialogOptions {
cancelLabel?: string
}
async function open(
options?: OpenDialogOptions & { multiple?: false, directory?: false }
): Promise<null | FileResponse>
async function open(
options?: OpenDialogOptions & { multiple?: true, directory?: false }
): Promise<null | FileResponse[]>
async function open(
options?: OpenDialogOptions & { multiple?: false, directory?: true }
): Promise<null | string>
async function open(
options?: OpenDialogOptions & { multiple?: true, directory?: true }
): Promise<null | string[]>
/**
* Open a file/directory selection dialog.
*
@ -140,7 +164,7 @@ interface ConfirmDialogOptions {
*/
async function open(
options: OpenDialogOptions = {}
): Promise<null | string | string[]> {
): Promise<null | string | string[] | FileResponse | FileResponse[]> {
if (typeof options === 'object') {
Object.freeze(options)
}

@ -7,13 +7,15 @@ use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use tauri::{command, Manager, Runtime, State, Window};
use crate::{Dialog, FileDialogBuilder, MessageDialogKind, Result};
use crate::{Dialog, FileDialogBuilder, FileResponse, MessageDialogKind, Result};
#[derive(Serialize)]
#[serde(untagged)]
pub enum OpenResponse {
Multiple(Option<Vec<PathBuf>>),
Path(Option<PathBuf>),
Folders(Option<Vec<PathBuf>>),
Folder(Option<PathBuf>),
Files(Option<Vec<FileResponse>>),
File(Option<FileResponse>),
}
#[allow(dead_code)]
@ -102,6 +104,8 @@ pub(crate) async fn open<R: Runtime>(
}
let res = if options.directory {
#[cfg(desktop)]
{
if options.multiple {
let folders = dialog_builder.blocking_pick_folders();
if let Some(folders) = &folders {
@ -111,38 +115,46 @@ pub(crate) async fn open<R: Runtime>(
.allow_directory(folder, options.recursive)?;
}
}
OpenResponse::Multiple(folders)
OpenResponse::Folders(folders)
} else {
let folder = dialog_builder.blocking_pick_folder();
if let Some(path) = &folder {
window.fs_scope().allow_directory(path, options.recursive)?;
}
OpenResponse::Path(folder)
OpenResponse::Folder(folder)
}
}
#[cfg(mobile)]
return Err(crate::Error::FolderPickerNotImplemented);
} else if options.multiple {
let files = dialog_builder.blocking_pick_files();
if let Some(files) = &files {
for file in files {
window.fs_scope().allow_file(file)?;
window.fs_scope().allow_file(&file.path)?;
}
}
OpenResponse::Multiple(files)
OpenResponse::Files(files)
} else {
let file = dialog_builder.blocking_pick_file();
if let Some(file) = &file {
window.fs_scope().allow_file(file)?;
window.fs_scope().allow_file(&file.path)?;
}
OpenResponse::Path(file)
OpenResponse::File(file)
};
Ok(res)
}
#[allow(unused_variables)]
#[command]
pub(crate) async fn save<R: Runtime>(
window: Window<R>,
dialog: State<'_, Dialog<R>>,
options: SaveDialogOptions,
) -> Result<Option<PathBuf>> {
#[cfg(mobile)]
return Err(crate::Error::FileSaveDialogNotImplemented);
#[cfg(desktop)]
{
let mut dialog_builder = dialog.file();
#[cfg(any(windows, target_os = "macos"))]
{
@ -165,6 +177,7 @@ pub(crate) async fn save<R: Runtime>(
}
Ok(path)
}
}
fn message_dialog<R: Runtime>(

@ -15,6 +15,12 @@ pub enum Error {
#[cfg(mobile)]
#[error(transparent)]
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
#[cfg(mobile)]
#[error("Folder picker is not implemented on mobile")]
FolderPickerNotImplemented,
#[cfg(mobile)]
#[error("File save dialog is not implemented on mobile")]
FileSaveDialogNotImplemented,
}
impl Serialize for Error {

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::Serialize;
use serde::{Deserialize, Serialize};
use tauri::{
plugin::{Builder, TauriPlugin},
Manager, Runtime,
@ -191,6 +191,36 @@ impl<R: Runtime> MessageDialogBuilder<R> {
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FileResponse {
pub base64_data: Option<String>,
pub duration: Option<u64>,
pub height: Option<usize>,
pub width: Option<usize>,
pub mime_type: Option<String>,
pub modified_at: Option<u64>,
pub name: Option<String>,
pub path: PathBuf,
pub size: u64,
}
impl FileResponse {
fn new(path: PathBuf) -> Self {
Self {
base64_data: None,
duration: None,
height: None,
width: None,
mime_type: None,
modified_at: None,
name: path.file_name().map(|f| f.to_string_lossy().into_owned()),
path,
size: 0,
}
}
}
#[derive(Debug, Serialize)]
pub(crate) struct Filter {
pub name: String,
@ -217,9 +247,7 @@ pub struct FileDialogBuilder<R: Runtime> {
#[serde(rename_all = "camelCase")]
pub struct FileDialogPayload<'a> {
filters: &'a Vec<Filter>,
starting_directory: &'a Option<PathBuf>,
file_name: &'a Option<String>,
title: &'a Option<String>,
multiple: bool,
}
// raw window handle :(
@ -240,12 +268,10 @@ impl<R: Runtime> FileDialogBuilder<R> {
}
#[cfg(mobile)]
pub(crate) fn payload(&self) -> FileDialogPayload<'_> {
pub(crate) fn payload(&self, multiple: bool) -> FileDialogPayload<'_> {
FileDialogPayload {
filters: &self.filters,
starting_directory: &self.starting_directory,
file_name: &self.file_name,
title: &self.title,
multiple,
}
}
@ -308,7 +334,9 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// })
/// })
/// ```
pub fn pick_file<F: FnOnce(Option<PathBuf>) + Send + 'static>(self, f: F) {
pub fn pick_file<F: FnOnce(Option<FileResponse>) + Send + 'static>(self, f: F) {
#[cfg(desktop)]
let f = |path: Option<PathBuf>| f(path.map(FileResponse::new));
pick_file(self, f)
}
@ -330,7 +358,15 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// })
/// })
/// ```
pub fn pick_files<F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>(self, f: F) {
pub fn pick_files<F: FnOnce(Option<Vec<FileResponse>>) + Send + 'static>(self, f: F) {
#[cfg(desktop)]
let f = |paths: Option<Vec<PathBuf>>| {
f(paths.map(|p| {
p.into_iter()
.map(FileResponse::new)
.collect::<Vec<FileResponse>>()
}))
};
pick_files(self, f)
}
@ -352,6 +388,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// })
/// })
/// ```
#[cfg(desktop)]
pub fn pick_folder<F: FnOnce(Option<PathBuf>) + Send + 'static>(self, f: F) {
pick_folder(self, f)
}
@ -374,6 +411,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// })
/// })
/// ```
#[cfg(desktop)]
pub fn pick_folders<F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>(self, f: F) {
pick_folders(self, f)
}
@ -397,6 +435,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// })
/// })
/// ```
#[cfg(desktop)]
pub fn save_file<F: FnOnce(Option<PathBuf>) + Send + 'static>(self, f: F) {
save_file(self, f)
}
@ -419,7 +458,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// // the file path is `None` if the user closed the dialog
/// }
/// ```
pub fn blocking_pick_file(self) -> Option<PathBuf> {
pub fn blocking_pick_file(self) -> Option<FileResponse> {
blocking_fn!(self, pick_file)
}
@ -438,7 +477,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// // the file paths value is `None` if the user closed the dialog
/// }
/// ```
pub fn blocking_pick_files(self) -> Option<Vec<PathBuf>> {
pub fn blocking_pick_files(self) -> Option<Vec<FileResponse>> {
blocking_fn!(self, pick_files)
}
@ -457,6 +496,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// // the folder path is `None` if the user closed the dialog
/// }
/// ```
#[cfg(desktop)]
pub fn blocking_pick_folder(self) -> Option<PathBuf> {
blocking_fn!(self, pick_folder)
}
@ -476,6 +516,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// // the folder paths value is `None` if the user closed the dialog
/// }
/// ```
#[cfg(desktop)]
pub fn blocking_pick_folders(self) -> Option<Vec<PathBuf>> {
blocking_fn!(self, pick_folders)
}
@ -495,6 +536,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// // the file path is `None` if the user closed the dialog
/// }
/// ```
#[cfg(desktop)]
pub fn blocking_save_file(self) -> Option<PathBuf> {
blocking_fn!(self, save_file)
}

@ -10,7 +10,7 @@ use tauri::{
AppHandle, Runtime,
};
use crate::{FileDialogBuilder, MessageDialogBuilder};
use crate::{FileDialogBuilder, FileResponse, MessageDialogBuilder};
#[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "app.tauri.dialog";
@ -46,59 +46,43 @@ impl<R: Runtime> Dialog<R> {
}
}
pub fn pick_file<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>(
dialog: FileDialogBuilder<R>,
f: F,
) {
let res = dialog
.dialog
.0
.run_mobile_plugin::<Option<PathBuf>>("pickFile", dialog.payload());
f(res.unwrap_or_default())
}
pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>(
dialog: FileDialogBuilder<R>,
f: F,
) {
let res = dialog
.dialog
.0
.run_mobile_plugin::<Option<Vec<PathBuf>>>("pickFiles", dialog.payload());
f(res.unwrap_or_default())
}
pub fn pick_folder<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>(
dialog: FileDialogBuilder<R>,
f: F,
) {
let res = dialog
.dialog
.0
.run_mobile_plugin::<Option<PathBuf>>("pickFolder", dialog.payload());
f(res.unwrap_or_default())
#[derive(Debug, Deserialize)]
struct FilePickerResponse {
files: Vec<FileResponse>,
}
pub fn pick_folders<R: Runtime, F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>(
pub fn pick_file<R: Runtime, F: FnOnce(Option<FileResponse>) + Send + 'static>(
dialog: FileDialogBuilder<R>,
f: F,
) {
std::thread::spawn(move || {
let res = dialog
.dialog
.0
.run_mobile_plugin::<Option<Vec<PathBuf>>>("pickFolders", dialog.payload());
f(res.unwrap_or_default())
.run_mobile_plugin::<FilePickerResponse>("showFilePicker", dialog.payload(false));
if let Ok(response) = res {
f(Some(response.files.into_iter().next().unwrap()))
} else {
f(None)
}
});
}
pub fn save_file<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>(
pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<FileResponse>>) + Send + 'static>(
dialog: FileDialogBuilder<R>,
f: F,
) {
std::thread::spawn(move || {
let res = dialog
.dialog
.0
.run_mobile_plugin::<Option<PathBuf>>("saveFile", dialog.payload());
f(res.unwrap_or_default())
.run_mobile_plugin::<FilePickerResponse>("showFilePicker", dialog.payload(true));
if let Ok(response) = res {
f(Some(response.files))
} else {
f(None)
}
});
}
#[derive(Debug, Deserialize)]

@ -79,9 +79,6 @@ importers:
plugins/dialog/examples/tauri-app:
dependencies:
'@tauri-apps/api':
specifier: ^1.1.0
version: 1.2.0
tauri-plugin-dialog-api:
specifier: link:../../
version: link:../..

Loading…
Cancel
Save