mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 10:29:58 +02:00
Baseline Compose Multiplatform Iroh migration
This commit is contained in:
206
shared/src/commonMain/kotlin/com/vnidrop/app/App.kt
Normal file
206
shared/src/commonMain/kotlin/com/vnidrop/app/App.kt
Normal file
@@ -0,0 +1,206 @@
|
||||
package com.vnidrop.app
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeContentPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
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 kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun App() {
|
||||
val platform = remember { getPlatform() }
|
||||
val repository = remember { CoreRepository() }
|
||||
val state by repository.state.collectAsState()
|
||||
val scope = rememberCoroutineScope()
|
||||
var appDataDir by remember { mutableStateOf(platform.defaultCoreDataDir) }
|
||||
var sourcePath by remember { mutableStateOf("") }
|
||||
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("") }
|
||||
|
||||
MaterialTheme {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.safeContentPadding()
|
||||
.fillMaxSize()
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
item {
|
||||
Text("VniDrop Core", style = MaterialTheme.typography.headlineMedium)
|
||||
Text(platform.name, style = MaterialTheme.typography.bodySmall)
|
||||
Text(state.status, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
|
||||
item {
|
||||
CoreCard(title = "Initialize") {
|
||||
OutlinedTextField(
|
||||
value = appDataDir,
|
||||
onValueChange = { appDataDir = it },
|
||||
label = { Text("Core data directory") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Button(onClick = {
|
||||
scope.launch { repository.initialize(appDataDir) }
|
||||
}) {
|
||||
Text("Initialize core")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
CoreCard(title = "Share path") {
|
||||
OutlinedTextField(
|
||||
value = sourcePath,
|
||||
onValueChange = { sourcePath = it },
|
||||
label = { Text("Source file path") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = transferName,
|
||||
onValueChange = { transferName = it },
|
||||
label = { Text("Transfer name") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = senderName,
|
||||
onValueChange = { senderName = it },
|
||||
label = { Text("Sender name") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Button(
|
||||
enabled = state.isInitialized && sourcePath.isNotBlank(),
|
||||
onClick = {
|
||||
scope.launch {
|
||||
repository.sharePath(sourcePath, transferName, senderName)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text("Create share ticket")
|
||||
}
|
||||
state.lastShare?.let { share ->
|
||||
Text("Ticket")
|
||||
Text(share.ticket, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
CoreCard(title = "Receive") {
|
||||
OutlinedTextField(
|
||||
value = ticket,
|
||||
onValueChange = { ticket = it },
|
||||
label = { Text("Ticket") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = 3,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = receiveDir,
|
||||
onValueChange = { receiveDir = it },
|
||||
label = { Text("Output directory") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = receiverName,
|
||||
onValueChange = { receiverName = it },
|
||||
label = { Text("Receiver name") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Button(
|
||||
enabled = state.isInitialized && ticket.isNotBlank(),
|
||||
onClick = { scope.launch { repository.inspectTicket(ticket) } },
|
||||
) {
|
||||
Text("Inspect ticket")
|
||||
}
|
||||
Button(
|
||||
enabled = state.isInitialized && ticket.isNotBlank() && receiveDir.isNotBlank(),
|
||||
onClick = {
|
||||
scope.launch { repository.receive(ticket, receiveDir, receiverName) }
|
||||
},
|
||||
) {
|
||||
Text("Receive")
|
||||
}
|
||||
state.lastInspection?.let { inspection ->
|
||||
Text("Kind: ${inspection.kind}")
|
||||
Text("Blob ticket: ${inspection.blobTicket.take(96)}")
|
||||
inspection.metadata?.let { metadata ->
|
||||
Text("${metadata.transferName} | ${metadata.fileCount} files | ${metadata.totalSize} bytes")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.error?.let { error ->
|
||||
item {
|
||||
Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)) {
|
||||
Text(
|
||||
text = error,
|
||||
modifier = Modifier.padding(12.dp),
|
||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Text("Events", style = MaterialTheme.typography.titleMedium)
|
||||
}
|
||||
items(state.events, key = { it.id }) { event ->
|
||||
Card {
|
||||
Text(
|
||||
text = "${event.scope}/${event.direction ?: "-"} ${event.phase}:${event.kind}",
|
||||
modifier = Modifier.padding(start = 12.dp, top = 10.dp, end = 12.dp),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = event.dataJson,
|
||||
modifier = Modifier.padding(start = 12.dp, bottom = 10.dp, end = 12.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CoreCard(
|
||||
title: String,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Text(title, style = MaterialTheme.typography.titleMedium)
|
||||
HorizontalDivider()
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
9
shared/src/commonMain/kotlin/com/vnidrop/app/Greeting.kt
Normal file
9
shared/src/commonMain/kotlin/com/vnidrop/app/Greeting.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package com.vnidrop.app
|
||||
|
||||
class Greeting {
|
||||
private val platform = getPlatform()
|
||||
|
||||
fun greet(): String {
|
||||
return sayHello(platform.name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.vnidrop.app
|
||||
|
||||
fun sayHello(to: String): String =
|
||||
"Hello, $to!"
|
||||
9
shared/src/commonMain/kotlin/com/vnidrop/app/Platform.kt
Normal file
9
shared/src/commonMain/kotlin/com/vnidrop/app/Platform.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package com.vnidrop.app
|
||||
|
||||
interface Platform {
|
||||
val name: String
|
||||
val defaultCoreDataDir: String
|
||||
val defaultReceiveDir: String
|
||||
}
|
||||
|
||||
expect fun getPlatform(): Platform
|
||||
@@ -0,0 +1,114 @@
|
||||
package com.vnidrop.app.core
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.random.Random
|
||||
import uniffi.vnidrop_core.CoreEvent
|
||||
import uniffi.vnidrop_core.CoreEventSink
|
||||
import uniffi.vnidrop_core.ShareMetadataInput
|
||||
import uniffi.vnidrop_core.ShareResult
|
||||
import uniffi.vnidrop_core.ShareSource
|
||||
import uniffi.vnidrop_core.SourceKind
|
||||
import uniffi.vnidrop_core.TicketInspection
|
||||
import uniffi.vnidrop_core.VnidropCore
|
||||
|
||||
data class CoreUiState(
|
||||
val isInitialized: Boolean = false,
|
||||
val status: String = "Not initialized",
|
||||
val events: List<CoreEvent> = emptyList(),
|
||||
val lastShare: ShareResult? = null,
|
||||
val lastInspection: TicketInspection? = null,
|
||||
val error: String? = null,
|
||||
)
|
||||
|
||||
class CoreRepository(
|
||||
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) {
|
||||
private val _state = MutableStateFlow(CoreUiState())
|
||||
val state: StateFlow<CoreUiState> = _state
|
||||
|
||||
private var core: VnidropCore? = null
|
||||
|
||||
private val sink = object : CoreEventSink {
|
||||
override fun onEvent(event: CoreEvent) {
|
||||
_state.update { current ->
|
||||
current.copy(events = (listOf(event) + current.events).take(200))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun initialize(appDataDir: String) = runCore {
|
||||
core?.shutdown()
|
||||
core = VnidropCore.initialize(appDataDir, sink)
|
||||
refreshStatus()
|
||||
_state.update { it.copy(isInitialized = true, error = null) }
|
||||
}
|
||||
|
||||
suspend fun sharePath(path: String, transferName: String, senderName: String) = runCore {
|
||||
val active = requireCore()
|
||||
val result = active.shareFiles(
|
||||
sources = listOf(
|
||||
ShareSource(
|
||||
kind = SourceKind.PATH,
|
||||
value = path,
|
||||
displayName = path.substringAfterLast('/').ifBlank { null },
|
||||
isDirectory = false,
|
||||
),
|
||||
),
|
||||
metadata = ShareMetadataInput(
|
||||
transferId = nextTransferId(),
|
||||
transferName = transferName.ifBlank { null },
|
||||
senderName = senderName.ifBlank { null },
|
||||
),
|
||||
)
|
||||
_state.update { it.copy(lastShare = result, error = null) }
|
||||
refreshStatus()
|
||||
}
|
||||
|
||||
suspend fun inspectTicket(ticket: String) = runCore {
|
||||
val inspection = requireCore().inspectTicket(ticket)
|
||||
_state.update { it.copy(lastInspection = inspection, error = null) }
|
||||
}
|
||||
|
||||
suspend fun receive(ticket: String, outputDir: String, receiverName: String) = runCore {
|
||||
requireCore().receive(ticket, outputDir, receiverName.ifBlank { null })
|
||||
refreshStatus()
|
||||
}
|
||||
|
||||
suspend fun cancel(transferId: ULong) = runCore {
|
||||
requireCore().cancelTransfer(transferId)
|
||||
refreshStatus()
|
||||
}
|
||||
|
||||
private suspend fun runCore(block: suspend () -> Unit) {
|
||||
withContext(dispatcher) {
|
||||
try {
|
||||
block()
|
||||
} catch (error: Throwable) {
|
||||
_state.update { it.copy(error = error.message ?: error.toString()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun requireCore(): VnidropCore =
|
||||
core ?: error("Initialize the core first.")
|
||||
|
||||
private fun refreshStatus() {
|
||||
val status = core?.status()
|
||||
_state.update {
|
||||
it.copy(
|
||||
status = status?.let { value ->
|
||||
"Endpoint ${value.endpointId.take(12)}... | active=${value.activeTransfers} shares=${value.activeShares}"
|
||||
} ?: "Not initialized",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun nextTransferId(): ULong =
|
||||
Random.nextLong(1, Long.MAX_VALUE).toULong()
|
||||
}
|
||||
Reference in New Issue
Block a user