mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 10:29:58 +02:00
fix(ui): present approvals modally and package fresh native builds
This commit is contained in:
@@ -58,6 +58,6 @@ android {
|
|||||||
|
|
||||||
tasks.configureEach {
|
tasks.configureEach {
|
||||||
if (name == "mergeDebugJniLibFolders" || name == "mergeDebugNativeLibs") {
|
if (name == "mergeDebugJniLibFolders" || name == "mergeDebugNativeLibs") {
|
||||||
dependsOn(":shared:cargoBuildAndroidArm64Debug")
|
dependsOn(":shared:copyAndroidAndroidArm64Debug")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import gobley.gradle.cargo.dsl.appleMobile
|
import gobley.gradle.cargo.dsl.appleMobile
|
||||||
import gobley.gradle.rust.targets.RustAndroidTarget
|
import gobley.gradle.rust.targets.RustAndroidTarget
|
||||||
|
import org.gradle.api.tasks.PathSensitivity
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
@@ -102,6 +103,18 @@ uniffi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.configureEach {
|
tasks.configureEach {
|
||||||
|
// Gobley does not currently treat every Rust source/API change as an input of
|
||||||
|
// all platform cargo tasks. Without these inputs an incremental Android build
|
||||||
|
// can package an older .so next to freshly generated UniFFI Kotlin bindings.
|
||||||
|
if (name.startsWith("cargoBuild")) {
|
||||||
|
inputs.files(
|
||||||
|
fileTree(layout.projectDirectory.dir("../crates/vnidrop")) {
|
||||||
|
include("Cargo.toml", "build.rs", "src/**/*.rs")
|
||||||
|
},
|
||||||
|
layout.projectDirectory.file("../Cargo.toml"),
|
||||||
|
layout.projectDirectory.file("../Cargo.lock"),
|
||||||
|
).withPathSensitivity(PathSensitivity.RELATIVE)
|
||||||
|
}
|
||||||
if (name.contains("Linux") || name.contains("MinGW") || name.contains("MacOSX64")) {
|
if (name.contains("Linux") || name.contains("MinGW") || name.contains("MacOSX64")) {
|
||||||
enabled = false
|
enabled = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ package com.vnidrop.app
|
|||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
@@ -15,10 +17,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vnidrop.app.feature.app.AppViewModel
|
import com.vnidrop.app.feature.app.AppViewModel
|
||||||
import com.vnidrop.app.feature.app.AppGraphViewModel
|
import com.vnidrop.app.feature.app.AppGraphViewModel
|
||||||
import com.vnidrop.app.feature.approvals.ApprovalBannerHost
|
import com.vnidrop.app.feature.approvals.ApprovalModalHost
|
||||||
import com.vnidrop.app.feature.receive.ReceiveRoute
|
import com.vnidrop.app.feature.receive.ReceiveRoute
|
||||||
import com.vnidrop.app.feature.receive.ReceiveViewModel
|
import com.vnidrop.app.feature.receive.ReceiveViewModel
|
||||||
import com.vnidrop.app.feature.send.SendRoute
|
import com.vnidrop.app.feature.send.SendRoute
|
||||||
|
import com.vnidrop.app.feature.send.SendFloatingAction
|
||||||
import com.vnidrop.app.feature.send.SendViewModel
|
import com.vnidrop.app.feature.send.SendViewModel
|
||||||
import com.vnidrop.app.feature.settings.SettingsRoute
|
import com.vnidrop.app.feature.settings.SettingsRoute
|
||||||
import com.vnidrop.app.feature.settings.SettingsViewModel
|
import com.vnidrop.app.feature.settings.SettingsViewModel
|
||||||
@@ -26,6 +29,9 @@ import com.vnidrop.app.platform.PlatformSystemAppearance
|
|||||||
import com.vnidrop.app.ui.feedback.VniDropSnackbarHost
|
import com.vnidrop.app.ui.feedback.VniDropSnackbarHost
|
||||||
import com.vnidrop.app.ui.navigation.AppDestination
|
import com.vnidrop.app.ui.navigation.AppDestination
|
||||||
import com.vnidrop.app.ui.shell.AppShell
|
import com.vnidrop.app.ui.shell.AppShell
|
||||||
|
import com.vnidrop.app.ui.shell.ScreenScrollContainer
|
||||||
|
import com.vnidrop.app.core.TransferDirection
|
||||||
|
import com.vnidrop.app.ui.state.WindowClass
|
||||||
import com.vnidrop.app.ui.state.windowClassFor
|
import com.vnidrop.app.ui.state.windowClassFor
|
||||||
import com.vnidrop.app.ui.theme.VniDropTheme
|
import com.vnidrop.app.ui.theme.VniDropTheme
|
||||||
import com.vnidrop.app.ui.theme.rememberResolvedDarkTheme
|
import com.vnidrop.app.ui.theme.rememberResolvedDarkTheme
|
||||||
@@ -55,6 +61,8 @@ fun App(dependencies: AppDependencies) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
val appState by appViewModel.state.collectAsStateWithLifecycle()
|
val appState by appViewModel.state.collectAsStateWithLifecycle()
|
||||||
|
val sendState by sendViewModel.state.collectAsStateWithLifecycle()
|
||||||
|
val sendCoreState by sendViewModel.coreState.collectAsStateWithLifecycle()
|
||||||
val approvalState by graph.approvalCoordinator.state.collectAsStateWithLifecycle()
|
val approvalState by graph.approvalCoordinator.state.collectAsStateWithLifecycle()
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
DisposableEffect(lifecycleOwner, graph, settingsViewModel) {
|
DisposableEffect(lifecycleOwner, graph, settingsViewModel) {
|
||||||
@@ -77,32 +85,42 @@ fun App(dependencies: AppDependencies) {
|
|||||||
VniDropTheme(isDarkTheme = darkTheme) {
|
VniDropTheme(isDarkTheme = darkTheme) {
|
||||||
BoxWithConstraints {
|
BoxWithConstraints {
|
||||||
val windowClass = windowClassFor(maxWidth.value)
|
val windowClass = windowClassFor(maxWidth.value)
|
||||||
Column(Modifier.fillMaxSize()) {
|
val showSendAction = appState.destination == AppDestination.Send &&
|
||||||
ApprovalBannerHost(
|
windowClass == WindowClass.Phone &&
|
||||||
state = approvalState,
|
sendState.selectedTransferId?.let { selectedId ->
|
||||||
onAccept = graph.approvalCoordinator::accept,
|
sendCoreState.transfers.any { it.transferId == selectedId }
|
||||||
onRefuse = graph.approvalCoordinator::refuse,
|
} != true &&
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
sendCoreState.transfers.any { it.direction == TransferDirection.Send }
|
||||||
)
|
|
||||||
AppShell(
|
AppShell(
|
||||||
modifier = Modifier.fillMaxSize().weight(1f),
|
modifier = Modifier.fillMaxSize(),
|
||||||
selectedDestination = appState.destination,
|
selectedDestination = appState.destination,
|
||||||
windowClass = windowClass,
|
windowClass = windowClass,
|
||||||
onDestinationSelected = appViewModel::selectDestination,
|
onDestinationSelected = appViewModel::selectDestination,
|
||||||
overlay = {
|
overlay = {
|
||||||
VniDropSnackbarHost(graph.messages, Modifier.align(Alignment.BottomCenter))
|
VniDropSnackbarHost(graph.messages, Modifier.align(Alignment.BottomCenter))
|
||||||
},
|
},
|
||||||
|
floatingAction = if (showSendAction) {
|
||||||
|
{
|
||||||
|
SendFloatingAction(
|
||||||
|
onClick = sendViewModel::openComposer,
|
||||||
|
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
when (appState.destination) {
|
when (appState.destination) {
|
||||||
AppDestination.Send -> SendRoute(sendViewModel, windowClass) { ticket ->
|
AppDestination.Send -> SendRoute(sendViewModel, windowClass)
|
||||||
receiveViewModel.setTicket(ticket)
|
AppDestination.Receive -> ScreenScrollContainer { ReceiveRoute(receiveViewModel) }
|
||||||
appViewModel.selectDestination(AppDestination.Receive)
|
AppDestination.Settings -> ScreenScrollContainer { SettingsRoute(settingsViewModel, windowClass) }
|
||||||
}
|
|
||||||
AppDestination.Receive -> ReceiveRoute(receiveViewModel)
|
|
||||||
AppDestination.Settings -> SettingsRoute(settingsViewModel, windowClass)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ApprovalModalHost(
|
||||||
|
state = approvalState,
|
||||||
|
onAccept = graph.approvalCoordinator::accept,
|
||||||
|
onRefuse = graph.approvalCoordinator::refuse,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
package com.vnidrop.app.feature.approvals
|
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.widthIn
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.vnidrop.app.ui.components.PrimaryButton
|
|
||||||
import com.vnidrop.app.ui.components.SecondaryButton
|
|
||||||
import com.vnidrop.app.ui.theme.LocalVniDropColors
|
|
||||||
import org.jetbrains.compose.resources.stringResource
|
|
||||||
import vnidrop.shared.generated.resources.Res
|
|
||||||
import vnidrop.shared.generated.resources.approval_connection_request
|
|
||||||
import vnidrop.shared.generated.resources.approval_pending_count
|
|
||||||
import vnidrop.shared.generated.resources.button_approve
|
|
||||||
import vnidrop.shared.generated.resources.button_refuse
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ApprovalBannerHost(
|
|
||||||
state: ApprovalState,
|
|
||||||
onAccept: (String) -> Unit,
|
|
||||||
onRefuse: (String) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val request = state.current ?: return
|
|
||||||
val busy = request.id in state.respondingIds
|
|
||||||
val receiver = request.receiverName ?: request.receiverDeviceName ?: "A nearby device"
|
|
||||||
val colors = LocalVniDropColors.current
|
|
||||||
Card(
|
|
||||||
modifier = modifier.widthIn(max = 640.dp).fillMaxWidth().padding(12.dp),
|
|
||||||
shape = RoundedCornerShape(14.dp),
|
|
||||||
colors = CardDefaults.cardColors(containerColor = colors.backgroundDialog),
|
|
||||||
border = BorderStroke(1.dp, colors.warningDefault),
|
|
||||||
) {
|
|
||||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
|
||||||
Text(stringResource(Res.string.approval_connection_request), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
|
||||||
Text("$receiver wants to receive ${request.transferName}.")
|
|
||||||
if (state.pending.size > 1) {
|
|
||||||
Text(stringResource(Res.string.approval_pending_count, state.pending.size), style = MaterialTheme.typography.bodySmall)
|
|
||||||
}
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
|
||||||
SecondaryButton(stringResource(Res.string.button_refuse), onClick = { onRefuse(request.id) }, enabled = !busy)
|
|
||||||
PrimaryButton(stringResource(Res.string.button_approve), onClick = { onAccept(request.id) }, enabled = !busy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,8 @@ package com.vnidrop.app.feature.approvals
|
|||||||
|
|
||||||
import com.vnidrop.app.core.CoreGateway
|
import com.vnidrop.app.core.CoreGateway
|
||||||
import com.vnidrop.app.core.CoreSignal
|
import com.vnidrop.app.core.CoreSignal
|
||||||
|
import com.vnidrop.app.core.TransferDirection
|
||||||
|
import com.vnidrop.app.core.TransferStatus
|
||||||
import com.vnidrop.app.core.ReceiverRequestModel
|
import com.vnidrop.app.core.ReceiverRequestModel
|
||||||
import com.vnidrop.app.notifications.LocalNotification
|
import com.vnidrop.app.notifications.LocalNotification
|
||||||
import com.vnidrop.app.notifications.LocalNotificationService
|
import com.vnidrop.app.notifications.LocalNotificationService
|
||||||
@@ -60,7 +62,7 @@ class ApprovalCoordinator(
|
|||||||
repository.state.collectLatest { core ->
|
repository.state.collectLatest { core ->
|
||||||
if (core.isInitialized) {
|
if (core.isInitialized) {
|
||||||
core.transfers
|
core.transfers
|
||||||
.filter { it.direction == "send" && it.status == "sharing" }
|
.filter { it.direction == TransferDirection.Send && it.status == TransferStatus.Sharing }
|
||||||
.forEach { refresh(it.transferId) }
|
.forEach { refresh(it.transferId) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.vnidrop.app.feature.approvals
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.graphics.vector.path
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import com.vnidrop.app.ui.components.PrimaryButton
|
||||||
|
import com.vnidrop.app.ui.components.SecondaryButton
|
||||||
|
import com.vnidrop.app.ui.theme.LocalVniDropColors
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import vnidrop.shared.generated.resources.Res
|
||||||
|
import vnidrop.shared.generated.resources.approval_connection_request
|
||||||
|
import vnidrop.shared.generated.resources.approval_pending_count
|
||||||
|
import vnidrop.shared.generated.resources.button_approve
|
||||||
|
import vnidrop.shared.generated.resources.button_refuse
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ApprovalModalHost(
|
||||||
|
state: ApprovalState,
|
||||||
|
onAccept: (String) -> Unit,
|
||||||
|
onRefuse: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
val request = state.current ?: return
|
||||||
|
val busy = request.id in state.respondingIds
|
||||||
|
val receiver = request.receiverName ?: request.receiverDeviceName ?: "A nearby device"
|
||||||
|
val colors = LocalVniDropColors.current
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = {},
|
||||||
|
properties = DialogProperties(
|
||||||
|
dismissOnBackPress = false,
|
||||||
|
dismissOnClickOutside = false,
|
||||||
|
usePlatformDefaultWidth = false,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.padding(24.dp).widthIn(max = 440.dp).fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(24.dp),
|
||||||
|
color = colors.backgroundDialog,
|
||||||
|
shadowElevation = 16.dp,
|
||||||
|
) {
|
||||||
|
Column(Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||||
|
Surface(shape = RoundedCornerShape(14.dp), color = colors.backgroundSelection) {
|
||||||
|
Icon(
|
||||||
|
ApprovalIcon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = colors.brandLink,
|
||||||
|
modifier = Modifier.padding(11.dp).size(24.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
stringResource(Res.string.approval_connection_request),
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"$receiver wants to receive ${request.transferName}.",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = colors.foregroundLight,
|
||||||
|
)
|
||||||
|
if (state.pending.size > 1) {
|
||||||
|
Text(
|
||||||
|
stringResource(Res.string.approval_pending_count, state.pending.size),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = colors.foregroundLighter,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
BoxWithConstraints(Modifier.fillMaxWidth()) {
|
||||||
|
if (maxWidth < 330.dp) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
PrimaryButton(stringResource(Res.string.button_approve), { onAccept(request.id) }, Modifier.fillMaxWidth(), !busy)
|
||||||
|
SecondaryButton(stringResource(Res.string.button_refuse), { onRefuse(request.id) }, Modifier.fillMaxWidth(), !busy)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
if (busy) CircularProgressIndicator(Modifier.size(20.dp), strokeWidth = 2.dp)
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
SecondaryButton(stringResource(Res.string.button_refuse), { onRefuse(request.id) }, enabled = !busy)
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
PrimaryButton(stringResource(Res.string.button_approve), { onAccept(request.id) }, enabled = !busy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val ApprovalIcon: ImageVector = ImageVector.Builder(
|
||||||
|
name = "Approval",
|
||||||
|
defaultWidth = 24.dp,
|
||||||
|
defaultHeight = 24.dp,
|
||||||
|
viewportWidth = 24f,
|
||||||
|
viewportHeight = 24f,
|
||||||
|
).apply {
|
||||||
|
path {
|
||||||
|
moveTo(12f, 2f); lineTo(20f, 5.5f); verticalLineTo(11f)
|
||||||
|
curveTo(20f, 16.1f, 16.6f, 20.7f, 12f, 22f)
|
||||||
|
curveTo(7.4f, 20.7f, 4f, 16.1f, 4f, 11f); verticalLineTo(5.5f); close()
|
||||||
|
moveTo(8.2f, 11.8f); lineTo(10.7f, 14.3f); lineTo(15.9f, 9.1f)
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
@@ -1,24 +1,44 @@
|
|||||||
package com.vnidrop.app.ui.feedback
|
package com.vnidrop.app.ui.feedback
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Snackbar
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SnackbarData
|
||||||
import androidx.compose.material3.SnackbarDuration
|
import androidx.compose.material3.SnackbarDuration
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.PathFillType
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.graphics.vector.path
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vnidrop.app.ui.theme.LocalVniDropColors
|
import com.vnidrop.app.ui.theme.LocalVniDropColors
|
||||||
import org.jetbrains.compose.resources.getString
|
import org.jetbrains.compose.resources.getString
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import vnidrop.shared.generated.resources.Res
|
||||||
|
import vnidrop.shared.generated.resources.snackbar_dismiss
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VniDropSnackbarHost(
|
fun VniDropSnackbarHost(
|
||||||
@@ -33,7 +53,7 @@ fun VniDropSnackbarHost(
|
|||||||
val result = hostState.showSnackbar(
|
val result = hostState.showSnackbar(
|
||||||
message = message.text.resolve(),
|
message = message.text.resolve(),
|
||||||
actionLabel = message.actionLabel?.resolve(),
|
actionLabel = message.actionLabel?.resolve(),
|
||||||
withDismissAction = message.actionLabel == null,
|
withDismissAction = true,
|
||||||
duration = if (message.tone == UiMessageTone.Error) SnackbarDuration.Long else SnackbarDuration.Short,
|
duration = if (message.tone == UiMessageTone.Error) SnackbarDuration.Long else SnackbarDuration.Short,
|
||||||
)
|
)
|
||||||
if (result == SnackbarResult.ActionPerformed) message.onAction?.invoke()
|
if (result == SnackbarResult.ActionPerformed) message.onAction?.invoke()
|
||||||
@@ -53,18 +73,88 @@ fun VniDropSnackbarHost(
|
|||||||
UiMessageTone.Warning -> colors.warningDefault
|
UiMessageTone.Warning -> colors.warningDefault
|
||||||
UiMessageTone.Error -> colors.destructiveDefault
|
UiMessageTone.Error -> colors.destructiveDefault
|
||||||
}
|
}
|
||||||
Snackbar(
|
Surface(
|
||||||
snackbarData = data,
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp).widthIn(max = 520.dp).fillMaxWidth(),
|
||||||
modifier = Modifier.widthIn(max = 520.dp).fillMaxWidth().padding(horizontal = 16.dp, vertical = 10.dp),
|
|
||||||
shape = RoundedCornerShape(10.dp),
|
shape = RoundedCornerShape(10.dp),
|
||||||
containerColor = colors.backgroundDialog,
|
color = colors.backgroundSurface200,
|
||||||
contentColor = colors.foregroundDefault,
|
contentColor = colors.foregroundDefault,
|
||||||
actionColor = accent,
|
shadowElevation = 6.dp,
|
||||||
dismissActionContentColor = colors.foregroundLighter,
|
) {
|
||||||
|
SnackbarContent(data, accent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SnackbarContent(data: SnackbarData, actionColor: Color) {
|
||||||
|
BoxWithConstraints {
|
||||||
|
val actionLabel = data.visuals.actionLabel
|
||||||
|
if (actionLabel != null && maxWidth < 420.dp) {
|
||||||
|
Column(Modifier.fillMaxWidth().padding(start = 16.dp, end = 6.dp, top = 8.dp, bottom = 6.dp)) {
|
||||||
|
MessageAndDismiss(data)
|
||||||
|
TextButton(onClick = data::performAction, modifier = Modifier.align(Alignment.End)) {
|
||||||
|
Text(actionLabel, color = actionColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 6.dp, top = 8.dp, bottom = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
SnackbarMessage(data.visuals.message, Modifier.weight(1f))
|
||||||
|
actionLabel?.let { label ->
|
||||||
|
TextButton(onClick = data::performAction) { Text(label, color = actionColor) }
|
||||||
|
}
|
||||||
|
DismissButton(data::dismiss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MessageAndDismiss(data: SnackbarData) {
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
SnackbarMessage(data.visuals.message, Modifier.weight(1f))
|
||||||
|
DismissButton(data::dismiss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SnackbarMessage(message: String, modifier: Modifier = Modifier) {
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
modifier = modifier.padding(vertical = 6.dp),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DismissButton(onClick: () -> Unit) {
|
||||||
|
IconButton(onClick = onClick, modifier = Modifier.size(40.dp)) {
|
||||||
|
Icon(
|
||||||
|
imageVector = CloseIcon,
|
||||||
|
contentDescription = stringResource(Res.string.snackbar_dismiss),
|
||||||
|
tint = LocalVniDropColors.current.foregroundLighter,
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val CloseIcon = ImageVector.Builder("Close", 24.dp, 24.dp, 24f, 24f).apply {
|
||||||
|
path(
|
||||||
|
fill = SolidColor(Color.Transparent),
|
||||||
|
stroke = SolidColor(Color.Black),
|
||||||
|
strokeLineWidth = 2f,
|
||||||
|
strokeLineCap = StrokeCap.Round,
|
||||||
|
pathFillType = PathFillType.NonZero,
|
||||||
|
) {
|
||||||
|
moveTo(6f, 6f)
|
||||||
|
lineTo(18f, 18f)
|
||||||
|
moveTo(18f, 6f)
|
||||||
|
lineTo(6f, 18f)
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
private suspend fun UiText.resolve(): String = when (this) {
|
private suspend fun UiText.resolve(): String = when (this) {
|
||||||
is UiText.Dynamic -> value
|
is UiText.Dynamic -> value
|
||||||
is UiText.Resource -> getString(resource)
|
is UiText.Resource -> getString(resource)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ fun AppShell(
|
|||||||
windowClass: WindowClass,
|
windowClass: WindowClass,
|
||||||
onDestinationSelected: (AppDestination) -> Unit,
|
onDestinationSelected: (AppDestination) -> Unit,
|
||||||
overlay: @Composable BoxScope.() -> Unit = {},
|
overlay: @Composable BoxScope.() -> Unit = {},
|
||||||
|
floatingAction: (@Composable BoxScope.() -> Unit)? = null,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val colors = LocalVniDropColors.current
|
val colors = LocalVniDropColors.current
|
||||||
@@ -42,6 +43,7 @@ fun AppShell(
|
|||||||
selectedDestination = selectedDestination,
|
selectedDestination = selectedDestination,
|
||||||
onDestinationSelected = onDestinationSelected,
|
onDestinationSelected = onDestinationSelected,
|
||||||
overlay = overlay,
|
overlay = overlay,
|
||||||
|
floatingAction = floatingAction,
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -49,6 +51,7 @@ fun AppShell(
|
|||||||
selectedDestination = selectedDestination,
|
selectedDestination = selectedDestination,
|
||||||
onDestinationSelected = onDestinationSelected,
|
onDestinationSelected = onDestinationSelected,
|
||||||
overlay = overlay,
|
overlay = overlay,
|
||||||
|
floatingAction = floatingAction,
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -60,6 +63,7 @@ private fun WideShell(
|
|||||||
selectedDestination: AppDestination,
|
selectedDestination: AppDestination,
|
||||||
onDestinationSelected: (AppDestination) -> Unit,
|
onDestinationSelected: (AppDestination) -> Unit,
|
||||||
overlay: @Composable BoxScope.() -> Unit,
|
overlay: @Composable BoxScope.() -> Unit,
|
||||||
|
floatingAction: (@Composable BoxScope.() -> Unit)?,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
Row(modifier = Modifier.fillMaxSize()) {
|
Row(modifier = Modifier.fillMaxSize()) {
|
||||||
@@ -68,8 +72,9 @@ private fun WideShell(
|
|||||||
onDestinationSelected = onDestinationSelected,
|
onDestinationSelected = onDestinationSelected,
|
||||||
)
|
)
|
||||||
Box(modifier = Modifier.weight(1f).fillMaxSize()) {
|
Box(modifier = Modifier.weight(1f).fillMaxSize()) {
|
||||||
ScreenScrollContainer(content = content)
|
content()
|
||||||
overlay()
|
floatingAction?.invoke(this)
|
||||||
|
Box(Modifier.fillMaxSize().padding(bottom = if (floatingAction == null) 0.dp else 72.dp)) { overlay() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,12 +84,14 @@ private fun PhoneShell(
|
|||||||
selectedDestination: AppDestination,
|
selectedDestination: AppDestination,
|
||||||
onDestinationSelected: (AppDestination) -> Unit,
|
onDestinationSelected: (AppDestination) -> Unit,
|
||||||
overlay: @Composable BoxScope.() -> Unit,
|
overlay: @Composable BoxScope.() -> Unit,
|
||||||
|
floatingAction: (@Composable BoxScope.() -> Unit)?,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
Box(modifier = Modifier.weight(1f).fillMaxSize()) {
|
Box(modifier = Modifier.weight(1f).fillMaxSize()) {
|
||||||
ScreenScrollContainer(content = content)
|
content()
|
||||||
overlay()
|
floatingAction?.invoke(this)
|
||||||
|
Box(Modifier.fillMaxSize().padding(bottom = if (floatingAction == null) 0.dp else 72.dp)) { overlay() }
|
||||||
}
|
}
|
||||||
AppBottomNavigation(
|
AppBottomNavigation(
|
||||||
selected = selectedDestination,
|
selected = selectedDestination,
|
||||||
|
|||||||
@@ -94,7 +94,21 @@ class ApprovalCoordinatorTest {
|
|||||||
respondedAt = null,
|
respondedAt = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun activeTransfer() = Transfer("local", 1UL, "send", "sharing", null, "Photos", 1UL, 1UL, null)
|
private fun activeTransfer() = Transfer(
|
||||||
|
localId = "local",
|
||||||
|
transferId = 1UL,
|
||||||
|
direction = com.vnidrop.app.core.TransferDirection.Send,
|
||||||
|
status = com.vnidrop.app.core.TransferStatus.Sharing,
|
||||||
|
peerId = null,
|
||||||
|
transferName = "Photos",
|
||||||
|
contentHash = "hash",
|
||||||
|
fileCount = 1UL,
|
||||||
|
totalSize = 1UL,
|
||||||
|
ticket = null,
|
||||||
|
accessPolicy = com.vnidrop.app.core.ShareAccessPolicy.RequireApproval,
|
||||||
|
createdAt = 1L,
|
||||||
|
updatedAt = 1L,
|
||||||
|
)
|
||||||
|
|
||||||
private fun preferences(enabled: Boolean) = FakePreferencesRepository(
|
private fun preferences(enabled: Boolean) = FakePreferencesRepository(
|
||||||
AppPreferences(
|
AppPreferences(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.vnidrop.app.ui
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -9,6 +10,7 @@ import androidx.compose.ui.test.assertIsDisplayed
|
|||||||
import androidx.compose.ui.test.assertCountEquals
|
import androidx.compose.ui.test.assertCountEquals
|
||||||
import androidx.compose.ui.test.ExperimentalTestApi
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
import androidx.compose.ui.test.getUnclippedBoundsInRoot
|
import androidx.compose.ui.test.getUnclippedBoundsInRoot
|
||||||
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
import androidx.compose.ui.test.onNodeWithTag
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.onAllNodesWithText
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
@@ -17,12 +19,20 @@ import androidx.compose.ui.platform.testTag
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.test.v2.runComposeUiTest
|
import androidx.compose.ui.test.v2.runComposeUiTest
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import com.vnidrop.app.feature.approvals.ApprovalBannerHost
|
import com.vnidrop.app.feature.approvals.ApprovalModalHost
|
||||||
import com.vnidrop.app.feature.approvals.ApprovalState
|
import com.vnidrop.app.feature.approvals.ApprovalState
|
||||||
import com.vnidrop.app.feature.approvals.PendingApproval
|
import com.vnidrop.app.feature.approvals.PendingApproval
|
||||||
import com.vnidrop.app.feature.settings.SettingsScreen
|
import com.vnidrop.app.feature.settings.SettingsScreen
|
||||||
import com.vnidrop.app.feature.settings.SettingsSection
|
import com.vnidrop.app.feature.settings.SettingsSection
|
||||||
import com.vnidrop.app.feature.settings.SettingsState
|
import com.vnidrop.app.feature.settings.SettingsState
|
||||||
|
import com.vnidrop.app.feature.send.SendScreen
|
||||||
|
import com.vnidrop.app.feature.send.SendState
|
||||||
|
import com.vnidrop.app.core.CoreState
|
||||||
|
import com.vnidrop.app.core.PickedShareFile
|
||||||
|
import com.vnidrop.app.core.ShareAccessPolicy
|
||||||
|
import com.vnidrop.app.core.Transfer
|
||||||
|
import com.vnidrop.app.core.TransferDirection
|
||||||
|
import com.vnidrop.app.core.TransferStatus
|
||||||
import com.vnidrop.app.notifications.NotificationPermission
|
import com.vnidrop.app.notifications.NotificationPermission
|
||||||
import com.vnidrop.app.ui.feedback.UiMessage
|
import com.vnidrop.app.ui.feedback.UiMessage
|
||||||
import com.vnidrop.app.ui.feedback.UiMessageController
|
import com.vnidrop.app.ui.feedback.UiMessageController
|
||||||
@@ -43,7 +53,7 @@ class FoundationComposeTest {
|
|||||||
var accepted: String? = null
|
var accepted: String? = null
|
||||||
setContent {
|
setContent {
|
||||||
VniDropTheme(isDarkTheme = false) {
|
VniDropTheme(isDarkTheme = false) {
|
||||||
ApprovalBannerHost(
|
ApprovalModalHost(
|
||||||
state = ApprovalState(pending = listOf(approval())),
|
state = ApprovalState(pending = listOf(approval())),
|
||||||
onAccept = { accepted = it },
|
onAccept = { accepted = it },
|
||||||
onRefuse = {},
|
onRefuse = {},
|
||||||
@@ -131,6 +141,30 @@ class FoundationComposeTest {
|
|||||||
VniDropTheme(isDarkTheme = false) { VniDropSnackbarHost(controller) }
|
VniDropTheme(isDarkTheme = false) { VniDropSnackbarHost(controller) }
|
||||||
}
|
}
|
||||||
onNodeWithText("Saved successfully").assertIsDisplayed()
|
onNodeWithText("Saved successfully").assertIsDisplayed()
|
||||||
|
onNodeWithContentDescription("Dismiss").assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun compactSnackbarMovesActionBelowMessageAndClose() = runComposeUiTest {
|
||||||
|
val controller = UiMessageController()
|
||||||
|
controller.tryShow(
|
||||||
|
UiMessage(
|
||||||
|
text = UiText.Dynamic("Notifications are turned off for VniDrop. You can enable them in Settings."),
|
||||||
|
actionLabel = UiText.Dynamic("Open Settings"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setContent {
|
||||||
|
VniDropTheme(isDarkTheme = false) {
|
||||||
|
Box(Modifier.width(320.dp)) { VniDropSnackbarHost(controller) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val messageBottom = onNodeWithText("Notifications are turned off for VniDrop. You can enable them in Settings.")
|
||||||
|
.getUnclippedBoundsInRoot().bottom
|
||||||
|
val closeBottom = onNodeWithContentDescription("Dismiss").getUnclippedBoundsInRoot().bottom
|
||||||
|
val actionTop = onNodeWithText("Open Settings").getUnclippedBoundsInRoot().top
|
||||||
|
assertTrue(messageBottom <= actionTop)
|
||||||
|
assertTrue(closeBottom <= actionTop)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -144,6 +178,9 @@ class FoundationComposeTest {
|
|||||||
overlay = {
|
overlay = {
|
||||||
Box(Modifier.align(Alignment.BottomCenter).size(20.dp).testTag("snackbar-overlay"))
|
Box(Modifier.align(Alignment.BottomCenter).size(20.dp).testTag("snackbar-overlay"))
|
||||||
},
|
},
|
||||||
|
floatingAction = {
|
||||||
|
Box(Modifier.align(Alignment.BottomEnd).size(56.dp).testTag("floating-action"))
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
Text("Content")
|
Text("Content")
|
||||||
}
|
}
|
||||||
@@ -151,10 +188,103 @@ class FoundationComposeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val overlayBottom = onNodeWithTag("snackbar-overlay").getUnclippedBoundsInRoot().bottom
|
val overlayBottom = onNodeWithTag("snackbar-overlay").getUnclippedBoundsInRoot().bottom
|
||||||
|
val floatingActionTop = onNodeWithTag("floating-action").getUnclippedBoundsInRoot().top
|
||||||
val navigationLabelTop = onNodeWithText("Send").getUnclippedBoundsInRoot().top
|
val navigationLabelTop = onNodeWithText("Send").getUnclippedBoundsInRoot().top
|
||||||
|
assertTrue(overlayBottom <= floatingActionTop)
|
||||||
assertTrue(overlayBottom <= navigationLabelTop)
|
assertTrue(overlayBottom <= navigationLabelTop)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun phoneSendEmptyStateOpensCreationDrawer() = runComposeUiTest {
|
||||||
|
val state = mutableStateOf(SendState())
|
||||||
|
setContent {
|
||||||
|
VniDropTheme(isDarkTheme = false) {
|
||||||
|
SendScreen(
|
||||||
|
coreState = CoreState(isInitialized = true),
|
||||||
|
state = state.value,
|
||||||
|
windowClass = WindowClass.Phone,
|
||||||
|
onOpenComposer = { state.value = state.value.copy(isComposerOpen = true) },
|
||||||
|
onDismissComposer = {},
|
||||||
|
onSelectFile = {},
|
||||||
|
onClearFile = {},
|
||||||
|
onTransferNameChanged = {},
|
||||||
|
onSenderNameChanged = {},
|
||||||
|
onAccessPolicyChanged = {},
|
||||||
|
onCreateShare = {},
|
||||||
|
onTransferSelected = {},
|
||||||
|
onCloseTransferDetails = {},
|
||||||
|
onCopyTicket = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeWithText("New transfer").performClick()
|
||||||
|
onNodeWithText("Choose what to share").assertIsDisplayed()
|
||||||
|
onNodeWithText("Choose file").assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun desktopTransferComposerReviewsFileAndAccessPolicy() = runComposeUiTest {
|
||||||
|
var selectedPolicy: ShareAccessPolicy? = null
|
||||||
|
setContent {
|
||||||
|
VniDropTheme(isDarkTheme = false) {
|
||||||
|
SendScreen(
|
||||||
|
coreState = CoreState(isInitialized = true),
|
||||||
|
state = SendState(
|
||||||
|
isComposerOpen = true,
|
||||||
|
selectedFile = PickedShareFile("/tmp/photos.zip", "photos.zip", 1536UL),
|
||||||
|
transferName = "photos.zip",
|
||||||
|
senderName = "Sender",
|
||||||
|
),
|
||||||
|
windowClass = WindowClass.Desktop,
|
||||||
|
onOpenComposer = {},
|
||||||
|
onDismissComposer = {},
|
||||||
|
onSelectFile = {},
|
||||||
|
onClearFile = {},
|
||||||
|
onTransferNameChanged = {},
|
||||||
|
onSenderNameChanged = {},
|
||||||
|
onAccessPolicyChanged = { selectedPolicy = it },
|
||||||
|
onCreateShare = {},
|
||||||
|
onTransferSelected = {},
|
||||||
|
onCloseTransferDetails = {},
|
||||||
|
onCopyTicket = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeWithText("1.5 KB").assertIsDisplayed()
|
||||||
|
onNodeWithText("Anyone with this transfer").performClick()
|
||||||
|
runOnIdle { assertEquals(ShareAccessPolicy.AnyoneWithTransfer, selectedPolicy) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun transferCatalogOpensSelectedTransfer() = runComposeUiTest {
|
||||||
|
var selectedId: ULong? = null
|
||||||
|
setContent {
|
||||||
|
VniDropTheme(isDarkTheme = false) {
|
||||||
|
SendScreen(
|
||||||
|
coreState = CoreState(isInitialized = true, transfers = listOf(outgoingTransfer())),
|
||||||
|
state = SendState(),
|
||||||
|
windowClass = WindowClass.Phone,
|
||||||
|
onOpenComposer = {},
|
||||||
|
onDismissComposer = {},
|
||||||
|
onSelectFile = {},
|
||||||
|
onClearFile = {},
|
||||||
|
onTransferNameChanged = {},
|
||||||
|
onSenderNameChanged = {},
|
||||||
|
onAccessPolicyChanged = {},
|
||||||
|
onCreateShare = {},
|
||||||
|
onTransferSelected = { selectedId = it },
|
||||||
|
onCloseTransferDetails = {},
|
||||||
|
onCopyTicket = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeWithText("Photos").performClick()
|
||||||
|
runOnIdle { assertEquals(9UL, selectedId) }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun snackbarActionAndCancellationAreForwarded() = runComposeUiTest {
|
fun snackbarActionAndCancellationAreForwarded() = runComposeUiTest {
|
||||||
val controller = UiMessageController()
|
val controller = UiMessageController()
|
||||||
@@ -184,4 +314,20 @@ class FoundationComposeTest {
|
|||||||
receiverDeviceName = "Phone",
|
receiverDeviceName = "Phone",
|
||||||
requestedAt = 1L,
|
requestedAt = 1L,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun outgoingTransfer() = Transfer(
|
||||||
|
localId = "local-9",
|
||||||
|
transferId = 9UL,
|
||||||
|
direction = TransferDirection.Send,
|
||||||
|
status = TransferStatus.Sharing,
|
||||||
|
peerId = null,
|
||||||
|
transferName = "Photos",
|
||||||
|
contentHash = "hash",
|
||||||
|
fileCount = 1UL,
|
||||||
|
totalSize = 1536UL,
|
||||||
|
ticket = "ticket",
|
||||||
|
accessPolicy = ShareAccessPolicy.RequireApproval,
|
||||||
|
createdAt = 1L,
|
||||||
|
updatedAt = 1L,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user