mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
Add native file picker to core test UI
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
package com.vnidrop.app.core
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
@Composable
|
||||
actual fun rememberShareFilePicker(
|
||||
onFilePicked: (PickedShareFile) -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
): ShareFilePicker {
|
||||
val context = LocalContext.current
|
||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||
if (uri != null) {
|
||||
onFilePicked(PickedShareFile(uri.toString(), context.displayName(uri)))
|
||||
}
|
||||
}
|
||||
return remember(launcher) {
|
||||
object : ShareFilePicker {
|
||||
override fun pickFile() {
|
||||
launcher.launch(arrayOf("*/*"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actual suspend fun sharePickedFile(
|
||||
repository: CoreRepository,
|
||||
file: PickedShareFile,
|
||||
transferName: String,
|
||||
senderName: String,
|
||||
) {
|
||||
val context = AndroidContextHolder.context
|
||||
errorIfNull(context, "Android context has not been attached")
|
||||
.contentResolver
|
||||
.openFileDescriptor(Uri.parse(file.value), "r")
|
||||
.use { descriptor ->
|
||||
checkNotNull(descriptor) { "Could not open selected file descriptor" }
|
||||
repository.shareFileDescriptor(
|
||||
fd = descriptor.fd,
|
||||
displayName = file.displayName,
|
||||
transferName = transferName,
|
||||
senderName = senderName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private object AndroidContextHolder {
|
||||
var context: Context? = null
|
||||
}
|
||||
|
||||
fun attachAndroidFilePickerContext(context: Context) {
|
||||
AndroidContextHolder.context = context.applicationContext
|
||||
}
|
||||
|
||||
private fun Context.displayName(uri: Uri): String {
|
||||
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (nameIndex >= 0 && cursor.moveToFirst()) {
|
||||
return cursor.getString(nameIndex)
|
||||
}
|
||||
}
|
||||
return uri.lastPathSegment ?: "transfer"
|
||||
}
|
||||
|
||||
private fun <T : Any> errorIfNull(value: T?, message: String): T =
|
||||
value ?: error(message)
|
||||
@@ -21,6 +21,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vnidrop.app.core.CoreRepository
|
||||
import com.vnidrop.app.core.PickedShareFile
|
||||
import com.vnidrop.app.core.rememberShareFilePicker
|
||||
import com.vnidrop.app.core.sharePickedFile
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
@@ -32,11 +35,22 @@ fun App() {
|
||||
val scope = rememberCoroutineScope()
|
||||
var appDataDir by remember { mutableStateOf(platform.defaultCoreDataDir) }
|
||||
var sourcePath by remember { mutableStateOf("") }
|
||||
var selectedFile by remember { mutableStateOf<PickedShareFile?>(null) }
|
||||
var transferName by remember { mutableStateOf("VniDrop transfer") }
|
||||
var senderName by remember { mutableStateOf("") }
|
||||
var ticket by remember { mutableStateOf("") }
|
||||
var receiveDir by remember { mutableStateOf(platform.defaultReceiveDir) }
|
||||
var receiverName by remember { mutableStateOf("") }
|
||||
val shareFilePicker = rememberShareFilePicker(
|
||||
onFilePicked = { file ->
|
||||
selectedFile = file
|
||||
sourcePath = file.value
|
||||
if (transferName.isBlank() || transferName == "VniDrop transfer") {
|
||||
transferName = file.displayName
|
||||
}
|
||||
},
|
||||
onError = { error -> scope.launch { repository.setError(error) } },
|
||||
)
|
||||
|
||||
MaterialTheme {
|
||||
Surface(
|
||||
@@ -73,13 +87,19 @@ fun App() {
|
||||
}
|
||||
|
||||
item {
|
||||
CoreCard(title = "Share path") {
|
||||
CoreCard(title = "Share file") {
|
||||
OutlinedTextField(
|
||||
value = sourcePath,
|
||||
onValueChange = { sourcePath = it },
|
||||
label = { Text("Source file path") },
|
||||
onValueChange = {
|
||||
sourcePath = it
|
||||
selectedFile = null
|
||||
},
|
||||
label = { Text("Selected file") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Button(onClick = { shareFilePicker.pickFile() }) {
|
||||
Text("Select file")
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = transferName,
|
||||
onValueChange = { transferName = it },
|
||||
@@ -93,10 +113,15 @@ fun App() {
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Button(
|
||||
enabled = state.isInitialized && sourcePath.isNotBlank(),
|
||||
enabled = state.isInitialized && (selectedFile != null || sourcePath.isNotBlank()),
|
||||
onClick = {
|
||||
scope.launch {
|
||||
repository.sharePath(sourcePath, transferName, senderName)
|
||||
val picked = selectedFile
|
||||
if (picked != null) {
|
||||
sharePickedFile(repository, picked, transferName, senderName)
|
||||
} else {
|
||||
repository.sharePath(sourcePath, transferName, senderName)
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
|
||||
@@ -135,6 +135,10 @@ class CoreRepository(
|
||||
refreshStatus()
|
||||
}
|
||||
|
||||
suspend fun setError(message: String) {
|
||||
_state.update { it.copy(error = message) }
|
||||
}
|
||||
|
||||
private suspend fun runCore(block: suspend () -> Unit) {
|
||||
withContext(dispatcher) {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.vnidrop.app.core
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
data class PickedShareFile(
|
||||
val value: String,
|
||||
val displayName: String,
|
||||
)
|
||||
|
||||
interface ShareFilePicker {
|
||||
fun pickFile()
|
||||
}
|
||||
|
||||
@Composable
|
||||
expect fun rememberShareFilePicker(
|
||||
onFilePicked: (PickedShareFile) -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
): ShareFilePicker
|
||||
|
||||
expect suspend fun sharePickedFile(
|
||||
repository: CoreRepository,
|
||||
file: PickedShareFile,
|
||||
transferName: String,
|
||||
senderName: String,
|
||||
)
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.vnidrop.app.core
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import platform.Foundation.NSURL
|
||||
import platform.UIKit.UIApplication
|
||||
import platform.UIKit.UIDocumentPickerDelegateProtocol
|
||||
import platform.UIKit.UIDocumentPickerViewController
|
||||
import platform.UIKit.UIModalPresentationFormSheet
|
||||
import platform.UniformTypeIdentifiers.UTTypeItem
|
||||
import platform.darwin.NSObject
|
||||
|
||||
private var retainedPickerDelegate: DocumentPickerDelegate? = null
|
||||
|
||||
@Composable
|
||||
actual fun rememberShareFilePicker(
|
||||
onFilePicked: (PickedShareFile) -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
): ShareFilePicker = remember(onFilePicked, onError) {
|
||||
object : ShareFilePicker {
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
override fun pickFile() {
|
||||
val presenter = UIApplication.sharedApplication.keyWindow?.rootViewController
|
||||
if (presenter == null) {
|
||||
onError("Could not find an iOS view controller for the document picker")
|
||||
return
|
||||
}
|
||||
|
||||
val picker = UIDocumentPickerViewController(forOpeningContentTypes = listOf(UTTypeItem), asCopy = false)
|
||||
val delegate = DocumentPickerDelegate(onFilePicked, onError)
|
||||
retainedPickerDelegate = delegate
|
||||
picker.delegate = delegate
|
||||
picker.modalPresentationStyle = UIModalPresentationFormSheet
|
||||
presenter.presentViewController(picker, animated = true, completion = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actual suspend fun sharePickedFile(
|
||||
repository: CoreRepository,
|
||||
file: PickedShareFile,
|
||||
transferName: String,
|
||||
senderName: String,
|
||||
) {
|
||||
repository.shareSecurityScopedFileUrl(file.value, file.displayName, transferName, senderName)
|
||||
}
|
||||
|
||||
private class DocumentPickerDelegate(
|
||||
private val onFilePicked: (PickedShareFile) -> Unit,
|
||||
private val onError: (String) -> Unit,
|
||||
) : NSObject(), UIDocumentPickerDelegateProtocol {
|
||||
override fun documentPicker(controller: UIDocumentPickerViewController, didPickDocumentsAtURLs: List<*>) {
|
||||
val url = didPickDocumentsAtURLs.firstOrNull() as? NSURL
|
||||
if (url == null) {
|
||||
onError("The selected iOS document URL was invalid")
|
||||
} else {
|
||||
val displayName = url.lastPathComponent ?: "transfer"
|
||||
onFilePicked(PickedShareFile(url.absoluteString ?: url.path.orEmpty(), displayName))
|
||||
}
|
||||
retainedPickerDelegate = null
|
||||
}
|
||||
|
||||
override fun documentPickerWasCancelled(controller: UIDocumentPickerViewController) {
|
||||
retainedPickerDelegate = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.vnidrop.app.core
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import java.awt.FileDialog
|
||||
import java.awt.Frame
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
actual fun rememberShareFilePicker(
|
||||
onFilePicked: (PickedShareFile) -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
): ShareFilePicker = remember(onFilePicked, onError) {
|
||||
object : ShareFilePicker {
|
||||
override fun pickFile() {
|
||||
try {
|
||||
val dialog = FileDialog(null as Frame?, "Select file to share", FileDialog.LOAD)
|
||||
dialog.isVisible = true
|
||||
val directory = dialog.directory
|
||||
val file = dialog.file
|
||||
if (directory != null && file != null) {
|
||||
val selected = File(directory, file)
|
||||
onFilePicked(PickedShareFile(selected.absolutePath, selected.name))
|
||||
}
|
||||
} catch (error: Throwable) {
|
||||
onError(error.message ?: error.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actual suspend fun sharePickedFile(
|
||||
repository: CoreRepository,
|
||||
file: PickedShareFile,
|
||||
transferName: String,
|
||||
senderName: String,
|
||||
) {
|
||||
repository.sharePath(file.value, transferName, senderName)
|
||||
}
|
||||
Reference in New Issue
Block a user