mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
feat(send): build durable transfer details and sharing flow
This commit is contained in:
@@ -5,7 +5,9 @@ import androidx.compose.runtime.remember
|
||||
import com.vnidrop.app.core.rememberFileSystemService
|
||||
import com.vnidrop.app.notifications.IosLocalNotificationService
|
||||
import platform.Foundation.NSBundle
|
||||
import platform.Foundation.NSTemporaryDirectory
|
||||
import platform.Foundation.NSApplicationSupportDirectory
|
||||
import platform.Foundation.NSSearchPathForDirectoriesInDomains
|
||||
import platform.Foundation.NSUserDomainMask
|
||||
import platform.UIKit.UIDevice
|
||||
|
||||
@Composable
|
||||
@@ -17,7 +19,7 @@ fun rememberIosAppDependencies(): AppDependencies {
|
||||
environment = PlatformEnvironment(
|
||||
name = device.systemName() + " " + device.systemVersion,
|
||||
appVersion = NSBundle.mainBundle.objectForInfoDictionaryKey("CFBundleShortVersionString") as? String ?: "0.1.0",
|
||||
defaultCoreDataDir = NSTemporaryDirectory() + "vnidrop",
|
||||
defaultCoreDataDir = iosApplicationDataDirectory(),
|
||||
defaultUsername = device.name.takeIf(String::isNotBlank) ?: "Receiver",
|
||||
),
|
||||
deviceInfoProvider = IosDeviceInfoProvider(device),
|
||||
@@ -27,6 +29,11 @@ fun rememberIosAppDependencies(): AppDependencies {
|
||||
}
|
||||
}
|
||||
|
||||
private fun iosApplicationDataDirectory(): String =
|
||||
(NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, true).firstOrNull() as? String)
|
||||
?.trimEnd('/')?.plus("/VniDrop")
|
||||
?: error("iOS Application Support directory is unavailable")
|
||||
|
||||
private class IosDeviceInfoProvider(
|
||||
private val device: UIDevice,
|
||||
) : DeviceInfoProvider {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.vnidrop.app.feature.send
|
||||
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.BetaInteropApi
|
||||
import kotlinx.cinterop.addressOf
|
||||
import kotlinx.cinterop.readBytes
|
||||
import kotlinx.cinterop.usePinned
|
||||
import platform.Foundation.NSData
|
||||
import platform.Foundation.NSDate
|
||||
import platform.Foundation.NSFileManager
|
||||
import platform.Foundation.NSFileModificationDate
|
||||
import platform.Foundation.NSFileSize
|
||||
import platform.Foundation.NSNumber
|
||||
import platform.Foundation.create
|
||||
import platform.Foundation.dataWithContentsOfFile
|
||||
import platform.Foundation.timeIntervalSince1970
|
||||
import platform.Foundation.writeToFile
|
||||
|
||||
actual fun createPlatformPreviewStore(appDataDir: String): PlatformPreviewStore =
|
||||
IosPreviewStore(appDataDir.trimEnd('/') + "/ui/previews")
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
|
||||
private class IosPreviewStore(private val directory: String) : PlatformPreviewStore {
|
||||
private val files = NSFileManager.defaultManager
|
||||
|
||||
override fun list(): List<PreviewFileInfo> {
|
||||
ensureDirectory()
|
||||
return files.contentsOfDirectoryAtPath(directory, null).orEmpty().filterIsInstance<String>().mapNotNull { name ->
|
||||
val id = name.removeSuffix(".preview").toULongOrNull() ?: return@mapNotNull null
|
||||
val attributes = files.attributesOfItemAtPath("$directory/$name", null) ?: return@mapNotNull null
|
||||
val size = (attributes[NSFileSize] as? NSNumber)?.longLongValue ?: 0L
|
||||
val modified = ((attributes[NSFileModificationDate] as? NSDate)?.timeIntervalSince1970 ?: 0.0) * 1000.0
|
||||
PreviewFileInfo(id, size, modified.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
override fun read(transferId: ULong): ByteArray? {
|
||||
val data = NSData.dataWithContentsOfFile(path(transferId)) ?: return null
|
||||
return data.bytes?.readBytes(data.length.toInt())
|
||||
}
|
||||
|
||||
override fun writeAtomically(transferId: ULong, bytes: ByteArray): Boolean {
|
||||
ensureDirectory()
|
||||
if (files.fileExistsAtPath(path(transferId))) return true
|
||||
val temporary = "$directory/.$transferId.tmp"
|
||||
val data = bytes.usePinned { pinned -> NSData.create(bytes = pinned.addressOf(0), length = bytes.size.toULong()) }
|
||||
if (!data.writeToFile(temporary, atomically = true)) return false
|
||||
val moved = files.moveItemAtPath(temporary, path(transferId), null)
|
||||
if (!moved) files.removeItemAtPath(temporary, null)
|
||||
return moved
|
||||
}
|
||||
|
||||
override fun delete(transferId: ULong) {
|
||||
files.removeItemAtPath(path(transferId), null)
|
||||
}
|
||||
|
||||
private fun ensureDirectory() {
|
||||
files.createDirectoryAtPath(directory, withIntermediateDirectories = true, attributes = null, error = null)
|
||||
}
|
||||
|
||||
private fun path(transferId: ULong) = "$directory/$transferId.preview"
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.vnidrop.app.feature.send
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.BetaInteropApi
|
||||
import platform.Foundation.NSString
|
||||
import platform.Foundation.NSTemporaryDirectory
|
||||
import platform.Foundation.NSURL
|
||||
import platform.Foundation.NSUTF8StringEncoding
|
||||
import platform.Foundation.create
|
||||
import platform.Foundation.writeToFile
|
||||
import platform.UIKit.UIActivityViewController
|
||||
import platform.UIKit.UIApplication
|
||||
import platform.UIKit.UIDocumentPickerViewController
|
||||
import platform.UIKit.UIModalPresentationFormSheet
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
@Composable
|
||||
actual fun rememberTransferShareActions(): TransferShareActions = remember {
|
||||
object : TransferShareActions {
|
||||
override val canUseNativeShare = true
|
||||
// Core NFC tag writing requires the NFC entitlement. Keep the action
|
||||
// visible but disabled until that capability is provisioned for the app.
|
||||
override val nfcAvailability = NfcShareAvailability.Unavailable
|
||||
|
||||
override fun exportInvitation(ticket: String, transferName: String, onResult: (Result<Unit>) -> Unit) {
|
||||
onResult(runCatching {
|
||||
val url = createInvitation(ticket, transferName)
|
||||
val picker = UIDocumentPickerViewController(forExportingURLs = listOf(url), asCopy = true)
|
||||
present(picker)
|
||||
})
|
||||
}
|
||||
|
||||
override fun shareInvitation(ticket: String, transferName: String, onResult: (Result<Unit>) -> Unit) {
|
||||
onResult(runCatching {
|
||||
val url = createInvitation(ticket, transferName)
|
||||
val controller = UIActivityViewController(activityItems = listOf(url), applicationActivities = null)
|
||||
controller.modalPresentationStyle = UIModalPresentationFormSheet
|
||||
presenter().presentViewController(controller, animated = true, completion = null)
|
||||
})
|
||||
}
|
||||
|
||||
override fun writeInvitationToNfc(ticket: String, onResult: (Result<Unit>) -> Unit) {
|
||||
onResult(Result.failure(UnsupportedOperationException("NFC tag writing is not enabled for this build")))
|
||||
}
|
||||
override fun cancelNfcWrite() = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
|
||||
private fun createInvitation(ticket: String, transferName: String): NSURL {
|
||||
val path = NSTemporaryDirectory().trimEnd('/') + "/" + invitationFileName(transferName)
|
||||
val text = NSString.create(string = ticket)
|
||||
require(text.writeToFile(path, atomically = true, encoding = NSUTF8StringEncoding, error = null)) {
|
||||
"The invitation file could not be created"
|
||||
}
|
||||
return NSURL.fileURLWithPath(path)
|
||||
}
|
||||
|
||||
private fun presenter() = UIApplication.sharedApplication.keyWindow?.rootViewController
|
||||
?: error("Could not find an iOS view controller")
|
||||
|
||||
private fun present(controller: platform.UIKit.UIViewController) {
|
||||
presenter().presentViewController(controller, animated = true, completion = null)
|
||||
}
|
||||
Reference in New Issue
Block a user