mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
feat(send): multi-file shares and receivers polish
Allow selecting multiple files on Android, desktop, and iOS and share them as one transfer through the core multi-source path. Update the composer UI for multi-file review, hide unfinished iOS receive methods, and surface refusal reasons on the receivers list.
This commit is contained in:
@@ -23,12 +23,12 @@ private var retainedPickerDelegate: DocumentPickerDelegate? = null
|
||||
|
||||
@Composable
|
||||
actual fun rememberShareFilePicker(
|
||||
onFilePicked: (PickedShareFile) -> Unit,
|
||||
onFilesPicked: (List<PickedShareFile>) -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
): ShareFilePicker = remember(onFilePicked, onError) {
|
||||
): ShareFilePicker = remember(onFilesPicked, onError) {
|
||||
object : ShareFilePicker {
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
override fun pickFile() {
|
||||
override fun pickFiles() {
|
||||
val presenter = UIApplication.sharedApplication.keyWindow?.rootViewController
|
||||
if (presenter == null) {
|
||||
onError("Could not find an iOS view controller for the document picker")
|
||||
@@ -36,7 +36,11 @@ actual fun rememberShareFilePicker(
|
||||
}
|
||||
|
||||
val picker = UIDocumentPickerViewController(forOpeningContentTypes = listOf(UTTypeItem), asCopy = false)
|
||||
val delegate = DocumentPickerDelegate(onFilePicked, onError)
|
||||
picker.allowsMultipleSelection = true
|
||||
val delegate = DocumentPickerDelegate(
|
||||
onFilesPicked = onFilesPicked,
|
||||
onError = onError,
|
||||
)
|
||||
retainedPickerDelegate = delegate
|
||||
picker.delegate = delegate
|
||||
picker.modalPresentationStyle = UIModalPresentationFormSheet
|
||||
@@ -61,7 +65,8 @@ actual fun rememberReceiveFolderPicker(
|
||||
|
||||
val picker = UIDocumentPickerViewController(forOpeningContentTypes = listOf(UTTypeFolder), asCopy = false)
|
||||
val delegate = DocumentPickerDelegate(
|
||||
onFilePicked = { folder ->
|
||||
onFilesPicked = { folders ->
|
||||
val folder = folders.firstOrNull() ?: return@DocumentPickerDelegate
|
||||
onFolderPicked(
|
||||
ReceiveFolder(
|
||||
kind = ReceiveFolderKind.IosSecurityScopedUrl,
|
||||
@@ -81,14 +86,12 @@ actual fun rememberReceiveFolderPicker(
|
||||
}
|
||||
|
||||
private class DocumentPickerDelegate(
|
||||
private val onFilePicked: (PickedShareFile) -> Unit,
|
||||
private val onFilesPicked: (List<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 files = didPickDocumentsAtURLs.mapNotNull { raw ->
|
||||
val url = raw as? NSURL ?: return@mapNotNull null
|
||||
val displayName = url.lastPathComponent ?: "transfer"
|
||||
val didStartAccess = url.startAccessingSecurityScopedResource()
|
||||
val sizeBytes = try {
|
||||
@@ -97,15 +100,18 @@ private class DocumentPickerDelegate(
|
||||
} finally {
|
||||
if (didStartAccess) url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
onFilePicked(
|
||||
PickedShareFile(
|
||||
url.absoluteString ?: url.path.orEmpty(),
|
||||
displayName,
|
||||
sizeBytes,
|
||||
nativeFileIcon(url),
|
||||
),
|
||||
PickedShareFile(
|
||||
url.absoluteString ?: url.path.orEmpty(),
|
||||
displayName,
|
||||
sizeBytes,
|
||||
nativeFileIcon(url),
|
||||
)
|
||||
}
|
||||
if (files.isEmpty()) {
|
||||
onError("The selected iOS document URL was invalid")
|
||||
} else {
|
||||
onFilesPicked(files)
|
||||
}
|
||||
retainedPickerDelegate = null
|
||||
}
|
||||
|
||||
|
||||
@@ -43,19 +43,24 @@ private class IosFileSystemService : FileSystemService {
|
||||
|
||||
override fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSink? = null
|
||||
|
||||
override suspend fun sharePickedFile(
|
||||
override suspend fun sharePickedFiles(
|
||||
repository: CoreGateway,
|
||||
file: PickedShareFile,
|
||||
files: List<PickedShareFile>,
|
||||
transferName: String,
|
||||
senderName: String,
|
||||
accessPolicy: ShareAccessPolicy,
|
||||
): Result<Share> = repository.shareSecurityScopedFileUrl(
|
||||
file.value,
|
||||
file.displayName,
|
||||
transferName,
|
||||
senderName,
|
||||
accessPolicy,
|
||||
)
|
||||
): Result<Share> {
|
||||
require(files.isNotEmpty()) { "Select at least one file to share" }
|
||||
val sources = files.map { file ->
|
||||
uniffi.vnidrop.ShareSource(
|
||||
kind = uniffi.vnidrop.SourceKind.IOS_SECURITY_SCOPED_URL,
|
||||
value = file.value,
|
||||
displayName = file.displayName,
|
||||
isDirectory = false,
|
||||
)
|
||||
}
|
||||
return repository.shareSources(sources, transferName, senderName, accessPolicy)
|
||||
}
|
||||
|
||||
private fun validateSecurityScopedUrl(value: String): FolderAccessStatus {
|
||||
val url = NSURL.URLWithString(value) ?: NSURL.fileURLWithPath(value)
|
||||
|
||||
@@ -19,8 +19,9 @@ private var retainedInvitationDelegate: InvitationDocumentDelegate? = null
|
||||
actual fun rememberReceiveInvitationActions(): ReceiveInvitationActions = remember {
|
||||
object : ReceiveInvitationActions {
|
||||
override val fileAvailability = ReceiveMethodAvailability.Available
|
||||
override val qrAvailability = ReceiveMethodAvailability.Unavailable
|
||||
override val nfcAvailability = ReceiveMethodAvailability.Unavailable
|
||||
// Hide unfinished iOS methods so the method list only shows what works.
|
||||
override val qrAvailability = ReceiveMethodAvailability.Hidden
|
||||
override val nfcAvailability = ReceiveMethodAvailability.Hidden
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
override fun pickInvitation(onResult: (Result<String>) -> Unit) {
|
||||
|
||||
Reference in New Issue
Block a user