mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 12:45:20 +00:00
receive
This commit is contained in:
@@ -13,6 +13,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -31,70 +32,87 @@ fun SuccessAnimation(
|
||||
onAnimationComplete: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var isVisible by remember { mutableStateOf(true) }
|
||||
var isVisible by remember { mutableStateOf(false) }
|
||||
var startExit by remember { mutableStateOf(false) }
|
||||
|
||||
// Auto-hide after animation duration
|
||||
// Control animation timing
|
||||
LaunchedEffect(animationData) {
|
||||
delay(2500) // Show for 2.5 seconds
|
||||
isVisible = false
|
||||
delay(300) // Allow fade out animation to complete
|
||||
// Start with fade in
|
||||
isVisible = true
|
||||
delay(2000) // Show for 2 seconds
|
||||
// Start exit animation
|
||||
startExit = true
|
||||
delay(500) // Allow fade out animation to complete
|
||||
onAnimationComplete()
|
||||
}
|
||||
|
||||
// Scale animation for the icon
|
||||
// Wobble/tada animation for the icon
|
||||
val infiniteTransition = rememberInfiniteTransition(label = "success_animation")
|
||||
val iconRotation by infiniteTransition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = if (isVisible && !startExit) 6f else 0f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 150, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "icon_wobble"
|
||||
)
|
||||
|
||||
val iconScale by infiniteTransition.animateFloat(
|
||||
initialValue = 1f,
|
||||
targetValue = 1.1f,
|
||||
targetValue = if (isVisible && !startExit) 1.05f else 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 1000, easing = FastOutSlowInEasing),
|
||||
animation = tween(durationMillis = 300, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "icon_scale"
|
||||
)
|
||||
|
||||
// Pulsing background effect
|
||||
val backgroundAlpha by infiniteTransition.animateFloat(
|
||||
initialValue = 0.9f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 1500, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
// Smooth fade in/out animations
|
||||
val contentScale by animateFloatAsState(
|
||||
targetValue = if (isVisible && !startExit) 1f else 0.9f,
|
||||
animationSpec = spring(
|
||||
dampingRatio = 0.7f,
|
||||
stiffness = Spring.StiffnessMedium
|
||||
),
|
||||
label = "background_alpha"
|
||||
)
|
||||
|
||||
// Entry animation
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (isVisible) 1f else 0.8f,
|
||||
animationSpec = spring(dampingRatio = 0.6f, stiffness = Spring.StiffnessLow),
|
||||
label = "entry_scale"
|
||||
label = "content_scale"
|
||||
)
|
||||
|
||||
val alpha by animateFloatAsState(
|
||||
targetValue = if (isVisible) 1f else 0f,
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
label = "entry_alpha"
|
||||
targetValue = if (isVisible && !startExit) 1f else 0f,
|
||||
animationSpec = tween(
|
||||
durationMillis = if (startExit) 400 else 600,
|
||||
easing = if (startExit) FastOutLinearInEasing else LinearOutSlowInEasing
|
||||
),
|
||||
label = "content_alpha"
|
||||
)
|
||||
|
||||
val backgroundAlpha by animateFloatAsState(
|
||||
targetValue = if (isVisible && !startExit) 0.95f else 0f,
|
||||
animationSpec = tween(
|
||||
durationMillis = if (startExit) 400 else 600
|
||||
),
|
||||
label = "background_alpha"
|
||||
)
|
||||
|
||||
if (alpha > 0f) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = backgroundAlpha * alpha)),
|
||||
.background(Color.Black.copy(alpha = backgroundAlpha)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
modifier = Modifier.scale(scale)
|
||||
modifier = Modifier.scale(contentScale)
|
||||
) {
|
||||
// Success icon with pulsing circle background
|
||||
// Success icon with wobble/tada effect
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.size(120.dp)
|
||||
) {
|
||||
// Pulsing background circle
|
||||
// Outer glow circle
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(100.dp)
|
||||
@@ -104,7 +122,7 @@ fun SuccessAnimation(
|
||||
)
|
||||
)
|
||||
|
||||
// Inner circle
|
||||
// Main success circle with wobble
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
@@ -112,7 +130,10 @@ fun SuccessAnimation(
|
||||
Color(0xFF00C851),
|
||||
CircleShape
|
||||
)
|
||||
.scale(iconScale),
|
||||
.scale(iconScale)
|
||||
.graphicsLayer {
|
||||
rotationZ = iconRotation
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
@@ -124,62 +145,202 @@ fun SuccessAnimation(
|
||||
}
|
||||
}
|
||||
|
||||
// Success message
|
||||
Text(
|
||||
text = "SUCCESS!",
|
||||
color = Color(0xFF00C851),
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
letterSpacing = 2.sp
|
||||
)
|
||||
|
||||
// Amount
|
||||
Text(
|
||||
text = formatAmount(animationData.amount, animationData.unit),
|
||||
color = Color.White,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
|
||||
// Description
|
||||
Text(
|
||||
text = animationData.description,
|
||||
color = Color.Gray,
|
||||
fontSize = 16.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 32.dp)
|
||||
)
|
||||
|
||||
// Animated checkmark or icon indicator
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
// Success message with smooth fade
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.graphicsLayer { this.alpha = alpha }
|
||||
) {
|
||||
for (i in 0..2) {
|
||||
val dotAlpha by infiniteTransition.animateFloat(
|
||||
initialValue = 0.3f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 600),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
initialStartOffset = StartOffset(i * 200)
|
||||
),
|
||||
label = "dot_$i"
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.background(
|
||||
Color(0xFF00C851).copy(alpha = dotAlpha),
|
||||
CircleShape
|
||||
)
|
||||
Text(
|
||||
text = "SUCCESS!",
|
||||
color = Color(0xFF00C851),
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
letterSpacing = 2.sp
|
||||
)
|
||||
|
||||
// Amount
|
||||
Text(
|
||||
text = formatAmount(animationData.amount, animationData.unit),
|
||||
color = Color.White,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
|
||||
// Description
|
||||
Text(
|
||||
text = animationData.description,
|
||||
color = Color.Gray,
|
||||
fontSize = 16.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fullscreen failure animation component for wallet operations
|
||||
*/
|
||||
@Composable
|
||||
fun FailureAnimation(
|
||||
errorMessage: String,
|
||||
operationType: String = "Operation",
|
||||
onAnimationComplete: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var isVisible by remember { mutableStateOf(false) }
|
||||
var startExit by remember { mutableStateOf(false) }
|
||||
|
||||
// Control animation timing
|
||||
LaunchedEffect(errorMessage) {
|
||||
// Start with fade in
|
||||
isVisible = true
|
||||
delay(3000) // Show for 3 seconds (longer for error message)
|
||||
// Start exit animation
|
||||
startExit = true
|
||||
delay(500) // Allow fade out animation to complete
|
||||
onAnimationComplete()
|
||||
}
|
||||
|
||||
// Shake animation for the error icon
|
||||
val infiniteTransition = rememberInfiniteTransition(label = "failure_animation")
|
||||
val iconShake by infiniteTransition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = if (isVisible && !startExit) 3f else 0f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 100, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "icon_shake"
|
||||
)
|
||||
|
||||
val iconScale by infiniteTransition.animateFloat(
|
||||
initialValue = 1f,
|
||||
targetValue = if (isVisible && !startExit) 1.03f else 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 400, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "icon_scale"
|
||||
)
|
||||
|
||||
// Smooth fade in/out animations
|
||||
val contentScale by animateFloatAsState(
|
||||
targetValue = if (isVisible && !startExit) 1f else 0.9f,
|
||||
animationSpec = spring(
|
||||
dampingRatio = 0.7f,
|
||||
stiffness = Spring.StiffnessMedium
|
||||
),
|
||||
label = "content_scale"
|
||||
)
|
||||
|
||||
val alpha by animateFloatAsState(
|
||||
targetValue = if (isVisible && !startExit) 1f else 0f,
|
||||
animationSpec = tween(
|
||||
durationMillis = if (startExit) 400 else 600,
|
||||
easing = if (startExit) FastOutLinearInEasing else LinearOutSlowInEasing
|
||||
),
|
||||
label = "content_alpha"
|
||||
)
|
||||
|
||||
val backgroundAlpha by animateFloatAsState(
|
||||
targetValue = if (isVisible && !startExit) 0.95f else 0f,
|
||||
animationSpec = tween(
|
||||
durationMillis = if (startExit) 400 else 600
|
||||
),
|
||||
label = "background_alpha"
|
||||
)
|
||||
|
||||
if (alpha > 0f) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = backgroundAlpha)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
modifier = Modifier.scale(contentScale)
|
||||
) {
|
||||
// Error icon with shake effect
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.size(120.dp)
|
||||
) {
|
||||
// Outer glow circle (red)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(100.dp)
|
||||
.background(
|
||||
Color(0xFFFF4444).copy(alpha = 0.2f),
|
||||
CircleShape
|
||||
)
|
||||
)
|
||||
|
||||
// Main error circle with shake
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.background(
|
||||
Color(0xFFFF4444),
|
||||
CircleShape
|
||||
)
|
||||
.scale(iconScale)
|
||||
.graphicsLayer {
|
||||
translationX = iconShake
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = "Error",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Error message with smooth fade
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.graphicsLayer { this.alpha = alpha }
|
||||
) {
|
||||
Text(
|
||||
text = "FAILED",
|
||||
color = Color(0xFFFF4444),
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
letterSpacing = 2.sp
|
||||
)
|
||||
|
||||
// Operation type
|
||||
Text(
|
||||
text = "$operationType Failed",
|
||||
color = Color.White,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
|
||||
// Error message
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = Color.Gray,
|
||||
fontSize = 14.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ fun WalletScreen(
|
||||
val showReceiveDialog by walletViewModel.showReceiveDialog.observeAsState(false)
|
||||
val showSuccessAnimation by walletViewModel.showSuccessAnimation.observeAsState(false)
|
||||
val successAnimationData by walletViewModel.successAnimationData.observeAsState()
|
||||
val showFailureAnimation by walletViewModel.showFailureAnimation.observeAsState(false)
|
||||
val failureAnimationData by walletViewModel.failureAnimationData.observeAsState()
|
||||
|
||||
// Back handler for the wallet
|
||||
fun handleBackPress(): Boolean {
|
||||
@@ -135,6 +137,18 @@ fun WalletScreen(
|
||||
modifier = Modifier.zIndex(10f)
|
||||
)
|
||||
}
|
||||
|
||||
// Failure animation overlay
|
||||
if (showFailureAnimation && failureAnimationData != null) {
|
||||
FailureAnimation(
|
||||
errorMessage = failureAnimationData!!.errorMessage,
|
||||
operationType = failureAnimationData!!.operationType,
|
||||
onAnimationComplete = {
|
||||
walletViewModel.hideFailureAnimation()
|
||||
},
|
||||
modifier = Modifier.zIndex(10f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle dialog states - sync local view state with ViewModel dialog state
|
||||
|
||||
@@ -93,6 +93,13 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||
private val _successAnimationData = MutableLiveData<SuccessAnimationData?>(null)
|
||||
val successAnimationData: LiveData<SuccessAnimationData?> = _successAnimationData
|
||||
|
||||
// Failure animation state
|
||||
private val _showFailureAnimation = MutableLiveData<Boolean>(false)
|
||||
val showFailureAnimation: LiveData<Boolean> = _showFailureAnimation
|
||||
|
||||
private val _failureAnimationData = MutableLiveData<FailureAnimationData?>(null)
|
||||
val failureAnimationData: LiveData<FailureAnimationData?> = _failureAnimationData
|
||||
|
||||
// State management
|
||||
private var pollingJob: kotlinx.coroutines.Job? = null
|
||||
|
||||
@@ -109,6 +116,14 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||
val description: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Data for failure animation display
|
||||
*/
|
||||
data class FailureAnimationData(
|
||||
val errorMessage: String,
|
||||
val operationType: String = "Token Receive"
|
||||
)
|
||||
|
||||
enum class SuccessAnimationType {
|
||||
CASHU_RECEIVED,
|
||||
CASHU_SENT,
|
||||
@@ -434,6 +449,17 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||
_successAnimationData.value = null
|
||||
}
|
||||
|
||||
// Failure animation management
|
||||
fun showFailureAnimation(animationData: FailureAnimationData) {
|
||||
_failureAnimationData.value = animationData
|
||||
_showFailureAnimation.value = true
|
||||
}
|
||||
|
||||
fun hideFailureAnimation() {
|
||||
_showFailureAnimation.value = false
|
||||
_failureAnimationData.value = null
|
||||
}
|
||||
|
||||
// Back navigation handler
|
||||
private var backHandler: (() -> Boolean)? = null
|
||||
|
||||
@@ -560,11 +586,54 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||
|
||||
}.onFailure { error ->
|
||||
Log.e(TAG, "Failed to save transaction", error)
|
||||
_errorMessage.value = "Failed to save transaction: ${error.message}"
|
||||
|
||||
// Clear token input and close dialog
|
||||
clearTokenInput()
|
||||
|
||||
// Show failure animation for transaction save error
|
||||
val failureData = FailureAnimationData(
|
||||
errorMessage = "Failed to save transaction: ${error.message}",
|
||||
operationType = "Token Receive"
|
||||
)
|
||||
showFailureAnimation(failureData)
|
||||
|
||||
// Close receive dialog after animation starts
|
||||
delay(500)
|
||||
hideReceiveDialog()
|
||||
}
|
||||
}.onFailure { error ->
|
||||
_errorMessage.value = "Failed to receive token: ${error.message}"
|
||||
Log.e(TAG, "Failed to receive token", error)
|
||||
|
||||
// Clear token input and close dialog
|
||||
clearTokenInput()
|
||||
|
||||
// Show failure animation for token receive error
|
||||
val failureData = FailureAnimationData(
|
||||
errorMessage = error.message ?: "Unknown error occurred",
|
||||
operationType = "Token Receive"
|
||||
)
|
||||
showFailureAnimation(failureData)
|
||||
|
||||
// Close receive dialog after animation starts
|
||||
delay(500)
|
||||
hideReceiveDialog()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Exception in receiveCashuToken", e)
|
||||
|
||||
// Clear token input and close dialog
|
||||
clearTokenInput()
|
||||
|
||||
// Show failure animation for unexpected error
|
||||
val failureData = FailureAnimationData(
|
||||
errorMessage = e.message ?: "Unexpected error occurred",
|
||||
operationType = "Token Receive"
|
||||
)
|
||||
showFailureAnimation(failureData)
|
||||
|
||||
// Close receive dialog after animation starts
|
||||
delay(500)
|
||||
hideReceiveDialog()
|
||||
} finally {
|
||||
_isLoading.value = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user