diff --git a/CHANGELOG.md b/CHANGELOG.md index 02ccde42a..a673a9c01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Fixed +- Polish Primary, Secondary, and Tertiary buttons to match Figma design specs #887 - Avoid msat truncation when paying invoices and LNURL callbacks #879 - Fix ANR on RGS server settings screen caused by catastrophic regex backtracking #880 - Fix crash when returning app to foreground on Receive screen #875 diff --git a/app/src/main/java/to/bitkit/di/HttpModule.kt b/app/src/main/java/to/bitkit/di/HttpModule.kt index f652939b3..4a1f551c6 100644 --- a/app/src/main/java/to/bitkit/di/HttpModule.kt +++ b/app/src/main/java/to/bitkit/di/HttpModule.kt @@ -18,9 +18,9 @@ import io.ktor.http.contentType import io.ktor.http.isSuccess import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json -import to.bitkit.utils.UrlValidator import to.bitkit.utils.AppError import to.bitkit.utils.Logger +import to.bitkit.utils.UrlValidator import javax.inject.Singleton import io.ktor.client.plugins.logging.Logger as KtorLogger diff --git a/app/src/main/java/to/bitkit/ui/Locals.kt b/app/src/main/java/to/bitkit/ui/Locals.kt index 2843e38c4..5e669eb46 100644 --- a/app/src/main/java/to/bitkit/ui/Locals.kt +++ b/app/src/main/java/to/bitkit/ui/Locals.kt @@ -53,6 +53,3 @@ val settingsViewModel: SettingsViewModel? val backupsViewModel: BackupsViewModel? @Composable get() = LocalBackupsViewModel.current - -val drawerState: DrawerState? - @Composable get() = LocalDrawerState.current diff --git a/app/src/main/java/to/bitkit/ui/components/Button.kt b/app/src/main/java/to/bitkit/ui/components/Button.kt index f64ef19db..dc91fb4f8 100644 --- a/app/src/main/java/to/bitkit/ui/components/Button.kt +++ b/app/src/main/java/to/bitkit/ui/components/Button.kt @@ -3,11 +3,13 @@ package to.bitkit.ui.components import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeight @@ -16,23 +18,32 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Home import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.HazeTint +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource +import dev.chrisbanes.haze.rememberHazeState +import to.bitkit.R import to.bitkit.ui.shared.modifiers.alphaFeedback +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.rememberDebouncedClick import to.bitkit.ui.shared.util.primaryButtonStyle import to.bitkit.ui.theme.AppButtonDefaults @@ -47,11 +58,35 @@ enum class ButtonSize { Small -> 40.dp Large -> 56.dp } - val horizontalPadding: Dp + val primaryHorizontalPadding: Dp get() = when (this) { Small -> 16.dp - Large -> 24.dp + Large -> 32.dp } + val primaryGap: Dp + get() = when (this) { + Small -> 8.dp + Large -> 6.dp + } + val primaryShadowElevation: Dp + get() = when (this) { + Small -> 4.dp + Large -> 16.dp + } + val secondaryHorizontalPadding: Dp + get() = when (this) { + Small -> 16.dp + Large -> 28.dp + } + val secondaryGap: Dp + get() = when (this) { + Small -> 8.dp + Large -> 6.dp + } + fun secondaryBorder(enabled: Boolean): BorderStroke = when (this) { + Large -> BorderStroke(2.dp, if (enabled) Colors.Gray4 else Color.Transparent) + Small -> BorderStroke(1.dp, if (enabled) Colors.White16 else Color.Transparent) + } } @Composable @@ -67,8 +102,8 @@ fun PrimaryButton( color: Color? = null, enableGradient: Boolean = true, ) { - val contentPadding = PaddingValues(horizontal = size.horizontalPadding.takeIf { text != null } ?: 0.dp) - val buttonShape = MaterialTheme.shapes.large + val contentPadding = PaddingValues(horizontal = size.primaryHorizontalPadding.takeIf { text != null } ?: 0.dp) + val buttonShape = MaterialTheme.shapes.extraLarge Button( onClick = rememberDebouncedClick(onClick = onClick), @@ -86,20 +121,20 @@ fun PrimaryButton( isEnabled = enabled && !isLoading, shape = buttonShape, primaryColor = color, - enableGradient = enableGradient + enableGradient = enableGradient, + shadowElevation = size.primaryShadowElevation, ) .alphaFeedback(enabled = enabled && !isLoading) ) { if (isLoading) { - CircularProgressIndicator( - color = Colors.White32, + GradientCircularProgressIndicator( strokeWidth = 2.dp, modifier = Modifier.size(size.height / 2) ) } else { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(size.primaryGap), ) { if (icon != null) { Box( @@ -136,49 +171,75 @@ fun SecondaryButton( size: ButtonSize = ButtonSize.Large, enabled: Boolean = true, fullWidth: Boolean = true, + hazeState: HazeState? = null, ) { - val contentPadding = PaddingValues(horizontal = size.horizontalPadding.takeIf { text != null } ?: 0.dp) - val border = BorderStroke(2.dp, if (enabled) Colors.Gray4 else Color.Transparent) - OutlinedButton( - onClick = rememberDebouncedClick(onClick = onClick), - enabled = enabled && !isLoading, - colors = AppButtonDefaults.secondaryColors, - contentPadding = contentPadding, - border = border, + val contentPadding = PaddingValues(horizontal = size.secondaryHorizontalPadding.takeIf { text != null } ?: 0.dp) + val border = size.secondaryBorder(enabled) + val contentColor = when (size) { + ButtonSize.Large -> Colors.White80 + ButtonSize.Small -> Colors.White64 + } + // hazeEffect must be on a Box wrapper (not OutlinedButton — Material's Surface draws over it) + // and AFTER size modifiers (Haze needs to know dimensions) + val buttonShape = MaterialTheme.shapes.extraLarge + Box( modifier = modifier .then(if (fullWidth) Modifier.fillMaxWidth() else Modifier) .requiredHeight(size.height) - ) { - if (isLoading) { - CircularProgressIndicator( - color = Colors.White32, - strokeWidth = 2.dp, - modifier = Modifier.size(size.height / 2) + .clip(buttonShape) + .then( + if (hazeState != null) { + Modifier.hazeEffect( + state = hazeState, + style = HazeStyle( + blurRadius = 12.dp, + backgroundColor = Color.Black, + tint = HazeTint(Color.Black.copy(alpha = 0.2f)), + ), + ) + } else { + Modifier + } ) - } else { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - if (icon != null) { - Box( - modifier = if (enabled) { - Modifier - } else { - Modifier.graphicsLayer { - colorFilter = ColorFilter.tint(Colors.White32) + ) { + OutlinedButton( + onClick = rememberDebouncedClick(onClick = onClick), + enabled = enabled && !isLoading, + colors = AppButtonDefaults.secondaryColors.copy(contentColor = contentColor), + contentPadding = contentPadding, + border = border, + modifier = if (fullWidth) Modifier.fillMaxSize() else Modifier, + ) { + if (isLoading) { + GradientCircularProgressIndicator( + strokeWidth = 2.dp, + modifier = Modifier.size(size.height / 2) + ) + } else { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(size.secondaryGap), + ) { + if (icon != null) { + Box( + modifier = if (enabled) { + Modifier + } else { + Modifier.graphicsLayer { + colorFilter = ColorFilter.tint(Colors.White32) + } } + ) { + icon() } - ) { - icon() } - } - text?.let { - Text( - text = text, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + text?.let { + Text( + text = text, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } } } } @@ -196,53 +257,51 @@ fun TertiaryButton( enabled: Boolean = true, fullWidth: Boolean = true, ) { - val contentPadding = PaddingValues(horizontal = size.horizontalPadding.takeIf { text != null } ?: 0.dp) - TextButton( - onClick = rememberDebouncedClick(onClick = onClick), - enabled = enabled && !isLoading, - colors = AppButtonDefaults.tertiaryColors, - contentPadding = contentPadding, + val contentColor = if (enabled && !isLoading) Colors.White80 else Colors.White32 + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), modifier = modifier .then(if (fullWidth) Modifier.fillMaxWidth() else Modifier) .requiredHeight(size.height) + .clickableAlpha( + enabled = enabled && !isLoading, + onClick = onClick, + ), ) { if (isLoading) { - CircularProgressIndicator( - color = Colors.White32, + GradientCircularProgressIndicator( strokeWidth = 2.dp, modifier = Modifier.size(size.height / 2) ) } else { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - if (icon != null) { - Box( - modifier = if (enabled) { - Modifier - } else { - Modifier.graphicsLayer { - colorFilter = ColorFilter.tint(Colors.White32) - } + if (icon != null) { + Box( + modifier = if (enabled) { + Modifier + } else { + Modifier.graphicsLayer { + colorFilter = ColorFilter.tint(Colors.White32) } - ) { - icon() } + ) { + icon() } - text?.let { - Text( - text = text, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } + } + text?.let { + Text( + text = text, + style = MaterialTheme.typography.labelLarge, + color = contentColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) } } } } -@Preview(showBackground = true) +@Preview @Composable private fun PrimaryButtonPreview() { AppThemeSurface { @@ -369,79 +428,39 @@ private fun PrimaryButtonPreview() { } } -@Preview(showBackground = true) +@Preview @Composable private fun SecondaryButtonPreview() { + val hazeState = rememberHazeState() AppThemeSurface { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.padding(16.dp) - ) { - SecondaryButton( - text = "Secondary", - onClick = {}, - ) - SecondaryButton( - text = "Secondary With padding", - modifier = Modifier.padding(horizontal = 32.dp), - onClick = {}, - ) - SecondaryButton( - text = "Secondary With Icon", - onClick = {}, - icon = { - Icon( - imageVector = Icons.Filled.Favorite, - contentDescription = "", - modifier = Modifier.size(16.dp) - ) - }, - ) - SecondaryButton( - text = "Secondary Loading", - isLoading = true, - onClick = {}, - ) - SecondaryButton( - text = "Secondary Disabled", - onClick = {}, - enabled = false, - icon = { - Icon( - imageVector = Icons.Filled.Favorite, - contentDescription = "", - modifier = Modifier.size(16.dp) - ) - }, - ) - SecondaryButton( - text = "Secondary Small", - size = ButtonSize.Small, - fullWidth = false, - onClick = {}, - ) - SecondaryButton( - text = "Secondary Small Loading", - size = ButtonSize.Small, - isLoading = true, - onClick = {}, - ) - SecondaryButton( - text = "Secondary Small Disabled", - size = ButtonSize.Small, - onClick = {}, - enabled = false, - ) - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() + Box { + Box( + modifier = Modifier + .matchParentSize() + .hazeSource(hazeState) + ) { + Image( + painter = painterResource(R.drawable.lightning), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.matchParentSize() + ) + } + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(16.dp) ) { + SecondaryButton(text = "Secondary", hazeState = hazeState, onClick = {}) SecondaryButton( - text = null, + text = "Secondary With padding", + hazeState = hazeState, onClick = {}, - fullWidth = false, - size = ButtonSize.Large, + modifier = Modifier.padding(horizontal = 32.dp) + ) + SecondaryButton( + text = "Secondary With Icon", + onClick = {}, + hazeState = hazeState, icon = { Icon( imageVector = Icons.Filled.Favorite, @@ -451,25 +470,84 @@ private fun SecondaryButtonPreview() { }, ) SecondaryButton( - text = null, + text = "Secondary Loading", + isLoading = true, + hazeState = hazeState, + onClick = {}, + ) + SecondaryButton( + text = "Secondary Disabled", onClick = {}, - fullWidth = false, - size = ButtonSize.Small, enabled = false, + hazeState = hazeState, icon = { Icon( - imageVector = Icons.Filled.Home, + imageVector = Icons.Filled.Favorite, contentDescription = "", modifier = Modifier.size(16.dp) ) }, ) + SecondaryButton( + text = "Secondary Small", + size = ButtonSize.Small, + fullWidth = false, + hazeState = hazeState, + onClick = {}, + ) + SecondaryButton( + text = "Secondary Small Loading", + size = ButtonSize.Small, + isLoading = true, + hazeState = hazeState, + onClick = {}, + ) + SecondaryButton( + text = "Secondary Small Disabled", + size = ButtonSize.Small, + onClick = {}, + enabled = false, + ) + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + SecondaryButton( + text = null, + onClick = {}, + fullWidth = false, + size = ButtonSize.Large, + hazeState = hazeState, + icon = { + Icon( + imageVector = Icons.Filled.Favorite, + contentDescription = "", + modifier = Modifier.size(16.dp) + ) + }, + ) + SecondaryButton( + text = null, + onClick = {}, + fullWidth = false, + size = ButtonSize.Small, + enabled = false, + icon = { + Icon( + imageVector = Icons.Filled.Home, + contentDescription = "", + modifier = Modifier.size(16.dp) + ) + }, + ) + } } } } } -@Preview(showBackground = true) +@Preview @Composable private fun TertiaryButtonPreview() { AppThemeSurface { diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt index 649f4732b..980581b0d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt @@ -5,13 +5,18 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -26,6 +31,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.synonym.bitkitcore.Activity +import dev.chrisbanes.haze.hazeSource +import dev.chrisbanes.haze.rememberHazeState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import to.bitkit.R @@ -68,22 +75,32 @@ fun SavingsWalletScreen( mutableStateOf(hasFunds && !isGeoBlocked) } + val hazeState = rememberHazeState() Box( modifier = Modifier .fillMaxSize() - .background(Colors.Black) .blockPointerInputPassthrough() ) { - Image( - painter = painterResource(id = R.drawable.piggybank), - contentDescription = null, - contentScale = ContentScale.Fit, + // Background layer: hazeSource must be a sibling of hazeEffect, not a parent. + // Haze can't blur an ancestor — source and effect must be at the same level. + Box( modifier = Modifier - .align(Alignment.TopEnd) - .padding(top = 32.dp) - .offset(x = (120).dp) - .size(268.dp) - ) + .matchParentSize() + .background(Colors.Black) + .hazeSource(hazeState) + ) { + Image( + painter = painterResource(id = R.drawable.piggybank), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 0.dp) + .offset(x = (160).dp) + .size(360.dp) + .windowInsetsPadding(WindowInsets.statusBars.only(WindowInsetsSides.Vertical)) + ) + } ScreenColumn(noBackground = true) { AppTopBar( titleText = stringResource(R.string.wallet__savings__title), @@ -108,7 +125,7 @@ fun SavingsWalletScreen( IncomingTransfer( amount = balances.balanceInTransferToSavings, remainingDuration = forceCloseRemainingDuration, - modifier = Modifier.padding(vertical = 8.dp), + modifier = Modifier.padding(vertical = 8.dp) ) } @@ -123,9 +140,10 @@ fun SavingsWalletScreen( Icon( painter = painterResource(R.drawable.ic_transfer), contentDescription = null, - modifier = Modifier.size(16.dp), + modifier = Modifier.size(16.dp) ) }, + hazeState = hazeState, modifier = Modifier.testTag("TransferToSpending") ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt index b10954e85..364abb8cf 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt @@ -5,13 +5,18 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -26,6 +31,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.synonym.bitkitcore.Activity +import dev.chrisbanes.haze.hazeSource +import dev.chrisbanes.haze.rememberHazeState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import org.lightningdevkit.ldknode.ChannelDetails @@ -70,21 +77,31 @@ fun SpendingWalletScreen( mutableStateOf(hasLnBalance && hasChannels) } + val hazeState = rememberHazeState() Box( modifier = Modifier .fillMaxSize() - .background(Colors.Black) .blockPointerInputPassthrough() ) { - Image( - painter = painterResource(id = R.drawable.coin_stack_x_2), - contentDescription = null, - contentScale = ContentScale.Fit, + // Background layer: hazeSource must be a sibling of hazeEffect, not a parent. + // Haze can't blur an ancestor — source and effect must be at the same level. + Box( modifier = Modifier - .align(Alignment.TopEnd) - .offset(x = (155).dp) - .size(330.dp) - ) + .matchParentSize() + .background(Colors.Black) + .hazeSource(hazeState) + ) { + Image( + painter = painterResource(id = R.drawable.coin_stack_x_2), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier + .align(Alignment.TopEnd) + .offset(x = (155).dp) + .size(330.dp) + .windowInsetsPadding(WindowInsets.statusBars.only(WindowInsetsSides.Vertical)) + ) + } ScreenColumn(noBackground = true) { AppTopBar( titleText = stringResource(R.string.wallet__spending__title), @@ -123,9 +140,10 @@ fun SpendingWalletScreen( Icon( painter = painterResource(R.drawable.ic_transfer), contentDescription = null, - modifier = Modifier.size(16.dp), + modifier = Modifier.size(16.dp) ) }, + hazeState = hazeState, modifier = Modifier.testTag("TransferToSavings") ) } @@ -142,7 +160,8 @@ fun SpendingWalletScreen( } if (showEmptyState) { EmptyStateView( - text = stringResource(R.string.wallet__spending__onboarding).withAccent(accentColor = Colors.Purple), + text = stringResource(R.string.wallet__spending__onboarding) + .withAccent(accentColor = Colors.Purple), modifier = Modifier .systemBarsPadding() .align(Alignment.BottomCenter) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 76d7ffb8a..7d10de0c3 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -376,7 +376,6 @@ private fun ReceiveQrView( size = ButtonSize.Small, onClick = onClickEditInvoice, fullWidth = false, - color = Colors.White10, icon = { Icon( painter = painterResource(R.drawable.ic_pencil_simple), @@ -402,7 +401,6 @@ private fun ReceiveQrView( coroutineScope.launch { qrButtonTooltipState.show() } }, fullWidth = true, - color = Colors.White10, icon = { Icon( painter = painterResource(R.drawable.ic_copy), @@ -425,7 +423,6 @@ private fun ReceiveQrView( } ?: shareText(context, copyText) }, fullWidth = false, - color = Colors.White10, icon = { Icon( painter = painterResource(R.drawable.ic_share), @@ -594,7 +591,6 @@ private fun CopyAddressCard( size = ButtonSize.Small, onClick = onClickEditInvoice, fullWidth = false, - color = Colors.White10, icon = { Icon( painter = painterResource(R.drawable.ic_pencil_simple), @@ -620,7 +616,6 @@ private fun CopyAddressCard( coroutineScope.launch { tooltipState.show() } }, fullWidth = false, - color = Colors.White10, icon = { Icon( painter = painterResource(R.drawable.ic_copy), @@ -637,7 +632,6 @@ private fun CopyAddressCard( size = ButtonSize.Small, onClick = { shareText(context, address) }, fullWidth = false, - color = Colors.White10, icon = { Icon( painter = painterResource(R.drawable.ic_share), diff --git a/app/src/main/java/to/bitkit/ui/shared/util/Modifiers.kt b/app/src/main/java/to/bitkit/ui/shared/util/Modifiers.kt index 709fc5694..e50243e66 100644 --- a/app/src/main/java/to/bitkit/ui/shared/util/Modifiers.kt +++ b/app/src/main/java/to/bitkit/ui/shared/util/Modifiers.kt @@ -12,12 +12,14 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.shadow import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.addOutline import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.pointer.pointerInput @@ -159,15 +161,16 @@ fun Modifier.primaryButtonStyle( shape: Shape, primaryColor: Color? = null, enableGradient: Boolean = true, + shadowElevation: Dp = 16.dp, ): Modifier { return this // Step 1: Add shadow (only when enabled) .then( if (isEnabled) { Modifier.shadow( - elevation = 16.dp, + elevation = shadowElevation, shape = shape, - clip = false // Don't clip content, just add shadow + clip = false, ) } else { Modifier @@ -175,50 +178,49 @@ fun Modifier.primaryButtonStyle( ) // Step 2: Clip to shape first .clip(shape) - // Step 3: Apply gradient background with border overlay - .then( + // Step 3: Apply background with optional gradient and shine + .drawWithContent { if (isEnabled) { - Modifier.drawWithContent { - // Draw the main background filling entire button - val baseColor = primaryColor ?: Colors.Gray5 - if (enableGradient) { - drawRect( - brush = Brush.verticalGradient( - colors = listOf(baseColor, Colors.Gray6), - startY = 0f, - endY = size.height - ), - topLeft = Offset.Zero, - size = size - ) - } else { - drawRect( - color = baseColor, - topLeft = Offset.Zero, - size = size - ) - } - - // Draw top border highlight (2dp gradient fade) - val borderHeight = 2.dp.toPx() + val baseColor = primaryColor ?: Colors.Gray5 + if (enableGradient) { drawRect( brush = Brush.verticalGradient( - colors = listOf( - Colors.White16, - Color.Transparent - ), + colors = listOf(baseColor, Colors.Gray6), startY = 0f, - endY = borderHeight + endY = size.height, ), - topLeft = Offset(0f, 0f), - size = Size(size.width, borderHeight) + topLeft = Offset.Zero, + size = size, + ) + } else { + drawRect( + color = baseColor, + topLeft = Offset.Zero, + size = size, ) - - // Draw the actual button content on top - drawContent() } + + // Draw top shine highlight following the rounded contour + val outline = shape.createOutline(size, layoutDirection, this) + val shinePath = Path() + shinePath.addOutline(outline) + drawPath( + path = shinePath, + brush = Brush.verticalGradient( + colors = listOf(Colors.White10, Color.Transparent), + startY = 0f, + endY = size.height * 0.15f, + ), + style = Stroke(width = 1.dp.toPx()), + ) } else { - Modifier.background(Colors.White06) + drawRect( + color = Colors.White06, + topLeft = Offset.Zero, + size = size, + ) } - ) + + drawContent() + } } diff --git a/app/src/main/java/to/bitkit/ui/theme/Defaults.kt b/app/src/main/java/to/bitkit/ui/theme/Defaults.kt index d7252c73e..1c7d71b12 100644 --- a/app/src/main/java/to/bitkit/ui/theme/Defaults.kt +++ b/app/src/main/java/to/bitkit/ui/theme/Defaults.kt @@ -66,6 +66,7 @@ object AppButtonDefaults { val secondaryColors: ButtonColors @Composable get() = ButtonDefaults.outlinedButtonColors( + containerColor = Colors.White.copy(alpha = 0.01f), contentColor = Colors.White80, disabledContentColor = Colors.White32, ) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 392c4b5e1..2eeed6077 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -73,11 +73,11 @@ import to.bitkit.env.Env import to.bitkit.ext.WatchResult import to.bitkit.ext.amountOnClose import to.bitkit.ext.amountSats +import to.bitkit.ext.callbackAmountMsats import to.bitkit.ext.channelId import to.bitkit.ext.claimableAtHeight import to.bitkit.ext.getClipboardText import to.bitkit.ext.getSatsPerVByteFor -import to.bitkit.ext.callbackAmountMsats import to.bitkit.ext.isFixedAmount import to.bitkit.ext.maxSendableSat import to.bitkit.ext.maxWithdrawableSat