mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-05-05 12:25:10 +02:00
feat(dialog): add plugin (#306)
This commit is contained in:
committed by
GitHub
parent
4d32919f0f
commit
7a8633f429
@@ -0,0 +1,205 @@
|
||||
// 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.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")
|
||||
val message = invoke.getString("message")
|
||||
val okButtonLabel = invoke.getString("okButtonLabel", "OK")
|
||||
val cancelButtonLabel = invoke.getString("cancelButtonLabel", "Cancel")
|
||||
|
||||
if (message == null) {
|
||||
invoke.reject("The `message` argument is required")
|
||||
return
|
||||
}
|
||||
|
||||
if (activity.isFinishing) {
|
||||
invoke.reject("App is finishing")
|
||||
return
|
||||
}
|
||||
|
||||
val handler = { cancelled: Boolean, value: Boolean ->
|
||||
val ret = JSObject()
|
||||
ret.put("cancelled", cancelled)
|
||||
ret.put("value", value)
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
|
||||
Handler(Looper.getMainLooper())
|
||||
.post {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
if (title != null) {
|
||||
builder.setTitle(title)
|
||||
}
|
||||
builder
|
||||
.setMessage(message)
|
||||
.setPositiveButton(
|
||||
okButtonLabel
|
||||
) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
handler(false, true)
|
||||
}
|
||||
.setNegativeButton(
|
||||
cancelButtonLabel
|
||||
) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
handler(false, false)
|
||||
}
|
||||
.setOnCancelListener { dialog ->
|
||||
dialog.dismiss()
|
||||
handler(true, false)
|
||||
}
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user