diff --git a/app/src/main/java/to/bitkit/data/SettingsStore.kt b/app/src/main/java/to/bitkit/data/SettingsStore.kt index 288a4d6b8..836395638 100644 --- a/app/src/main/java/to/bitkit/data/SettingsStore.kt +++ b/app/src/main/java/to/bitkit/data/SettingsStore.kt @@ -124,6 +124,7 @@ data class SettingsData( val backupWarningIgnoredMillis: Long = 0, val notificationsIgnoredMillis: Long = 0, val balanceWarningTimes: Int = 0, + val widgetsOnboardingHintDismissed: Boolean = false, val coinSelectAuto: Boolean = true, val coinSelectPreference: CoinSelectionPreference = CoinSelectionPreference.BranchAndBound, val electrumServer: String = Env.electrumServerUrl, diff --git a/app/src/main/java/to/bitkit/data/WidgetsStore.kt b/app/src/main/java/to/bitkit/data/WidgetsStore.kt index 13487b950..97ffaa39f 100644 --- a/app/src/main/java/to/bitkit/data/WidgetsStore.kt +++ b/app/src/main/java/to/bitkit/data/WidgetsStore.kt @@ -134,7 +134,11 @@ class WidgetsStore @Inject constructor( if (store.data.first().widgets.map { it.type }.contains(type)) return store.updateData { data -> - data.copy(widgets = (data.widgets + WidgetWithPosition(type = type)).sortedBy { it.position }) + val nextPosition = (data.widgets.maxOfOrNull { it.position } ?: -1) + 1 + data.copy( + widgets = (data.widgets + WidgetWithPosition(type = type, position = nextPosition)) + .sortedBy { it.position } + ) } } @@ -160,9 +164,9 @@ class WidgetsStore @Inject constructor( @Serializable data class WidgetsData( val widgets: List = listOf( - WidgetWithPosition(type = WidgetType.PRICE, position = 0), - WidgetWithPosition(type = WidgetType.BLOCK, position = 1), - WidgetWithPosition(type = WidgetType.NEWS, position = 2), + WidgetWithPosition(type = WidgetType.SUGGESTIONS, position = 0), + WidgetWithPosition(type = WidgetType.PRICE, position = 1), + WidgetWithPosition(type = WidgetType.BLOCK, position = 2), ), val headlinePreferences: HeadlinePreferences = HeadlinePreferences(), val factsPreferences: FactsPreferences = FactsPreferences(), diff --git a/app/src/main/java/to/bitkit/data/dto/price/PriceDTO.kt b/app/src/main/java/to/bitkit/data/dto/price/PriceDTO.kt index 0d1ba1c88..22243cf7b 100644 --- a/app/src/main/java/to/bitkit/data/dto/price/PriceDTO.kt +++ b/app/src/main/java/to/bitkit/data/dto/price/PriceDTO.kt @@ -1,9 +1,11 @@ package to.bitkit.data.dto.price +import androidx.compose.runtime.Stable import kotlinx.serialization.Serializable +@Stable @Serializable data class PriceDTO( - val widgets: List, + @Stable val widgets: List, val source: String ) diff --git a/app/src/main/java/to/bitkit/models/BalanceState.kt b/app/src/main/java/to/bitkit/models/BalanceState.kt index ec9d671ad..5907238e8 100644 --- a/app/src/main/java/to/bitkit/models/BalanceState.kt +++ b/app/src/main/java/to/bitkit/models/BalanceState.kt @@ -1,7 +1,9 @@ package to.bitkit.models +import androidx.compose.runtime.Immutable import kotlinx.serialization.Serializable +@Immutable @Serializable data class BalanceState( val totalOnchainSats: ULong = 0uL, diff --git a/app/src/main/java/to/bitkit/models/WidgetType.kt b/app/src/main/java/to/bitkit/models/WidgetType.kt index 95d0c74bc..00f26bce4 100644 --- a/app/src/main/java/to/bitkit/models/WidgetType.kt +++ b/app/src/main/java/to/bitkit/models/WidgetType.kt @@ -31,5 +31,9 @@ enum class WidgetType( WEATHER( iconRes = R.drawable.widget_cloud, title = R.string.widgets__weather__name + ), + SUGGESTIONS( + iconRes = R.drawable.widget_suggestions, + title = R.string.widgets__suggestions__name, ) } diff --git a/app/src/main/java/to/bitkit/models/WidgetWithPosition.kt b/app/src/main/java/to/bitkit/models/WidgetWithPosition.kt index b1ed0f51d..bb433bf17 100644 --- a/app/src/main/java/to/bitkit/models/WidgetWithPosition.kt +++ b/app/src/main/java/to/bitkit/models/WidgetWithPosition.kt @@ -1,7 +1,9 @@ package to.bitkit.models +import androidx.compose.runtime.Immutable import kotlinx.serialization.Serializable +@Immutable @Serializable data class WidgetWithPosition( val type: WidgetType, diff --git a/app/src/main/java/to/bitkit/models/widget/ArticleModel.kt b/app/src/main/java/to/bitkit/models/widget/ArticleModel.kt index 66494e0f2..a96e26949 100644 --- a/app/src/main/java/to/bitkit/models/widget/ArticleModel.kt +++ b/app/src/main/java/to/bitkit/models/widget/ArticleModel.kt @@ -1,5 +1,6 @@ package to.bitkit.models.widget +import androidx.compose.runtime.Immutable import kotlinx.serialization.Serializable import to.bitkit.data.dto.ArticleDTO import to.bitkit.ext.toRelativeTimeString @@ -12,6 +13,7 @@ import kotlin.time.ExperimentalTime private const val TAG = "ArticleModel" +@Immutable @Serializable data class ArticleModel( val title: String, diff --git a/app/src/main/java/to/bitkit/models/widget/BlockModel.kt b/app/src/main/java/to/bitkit/models/widget/BlockModel.kt index c5bba37c6..5d260a9d1 100644 --- a/app/src/main/java/to/bitkit/models/widget/BlockModel.kt +++ b/app/src/main/java/to/bitkit/models/widget/BlockModel.kt @@ -1,10 +1,12 @@ package to.bitkit.models.widget +import androidx.compose.runtime.Immutable import kotlinx.serialization.Serializable import to.bitkit.data.dto.BlockDTO import to.bitkit.ext.toDateUTC import to.bitkit.ext.toTimeUTC +@Immutable @Serializable data class BlockModel( val height: String, diff --git a/app/src/main/java/to/bitkit/models/widget/BlocksPreferences.kt b/app/src/main/java/to/bitkit/models/widget/BlocksPreferences.kt index 34afde4d5..2ae2d943e 100644 --- a/app/src/main/java/to/bitkit/models/widget/BlocksPreferences.kt +++ b/app/src/main/java/to/bitkit/models/widget/BlocksPreferences.kt @@ -1,7 +1,9 @@ package to.bitkit.models.widget +import androidx.compose.runtime.Immutable import kotlinx.serialization.Serializable +@Immutable @Serializable data class BlocksPreferences( val showBlock: Boolean = true, diff --git a/app/src/main/java/to/bitkit/models/widget/FactsPreferences.kt b/app/src/main/java/to/bitkit/models/widget/FactsPreferences.kt index 53182d31b..68adcf703 100644 --- a/app/src/main/java/to/bitkit/models/widget/FactsPreferences.kt +++ b/app/src/main/java/to/bitkit/models/widget/FactsPreferences.kt @@ -1,7 +1,9 @@ package to.bitkit.models.widget +import androidx.compose.runtime.Immutable import kotlinx.serialization.Serializable +@Immutable @Serializable data class FactsPreferences( val showSource: Boolean = false diff --git a/app/src/main/java/to/bitkit/models/widget/HeadlinePreferences.kt b/app/src/main/java/to/bitkit/models/widget/HeadlinePreferences.kt index 03eded621..93723103b 100644 --- a/app/src/main/java/to/bitkit/models/widget/HeadlinePreferences.kt +++ b/app/src/main/java/to/bitkit/models/widget/HeadlinePreferences.kt @@ -1,7 +1,9 @@ package to.bitkit.models.widget +import androidx.compose.runtime.Immutable import kotlinx.serialization.Serializable +@Immutable @Serializable data class HeadlinePreferences( val showTime: Boolean = true, diff --git a/app/src/main/java/to/bitkit/models/widget/PricePreferences.kt b/app/src/main/java/to/bitkit/models/widget/PricePreferences.kt index ae3f468ab..49d5fd013 100644 --- a/app/src/main/java/to/bitkit/models/widget/PricePreferences.kt +++ b/app/src/main/java/to/bitkit/models/widget/PricePreferences.kt @@ -1,12 +1,14 @@ package to.bitkit.models.widget +import androidx.compose.runtime.Stable import kotlinx.serialization.Serializable import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.TradingPair +@Stable @Serializable data class PricePreferences( - val enabledPairs: List = listOf( + @Stable val enabledPairs: List = listOf( TradingPair.BTC_USD ), val period: GraphPeriod? = GraphPeriod.ONE_DAY, diff --git a/app/src/main/java/to/bitkit/models/widget/WeatherPreferences.kt b/app/src/main/java/to/bitkit/models/widget/WeatherPreferences.kt index 4010cdf01..1151c37a6 100644 --- a/app/src/main/java/to/bitkit/models/widget/WeatherPreferences.kt +++ b/app/src/main/java/to/bitkit/models/widget/WeatherPreferences.kt @@ -1,7 +1,9 @@ package to.bitkit.models.widget +import androidx.compose.runtime.Immutable import kotlinx.serialization.Serializable +@Immutable @Serializable data class WeatherPreferences( val showTitle: Boolean = true, diff --git a/app/src/main/java/to/bitkit/repositories/WidgetsRepo.kt b/app/src/main/java/to/bitkit/repositories/WidgetsRepo.kt index 66f7b2338..fb5746535 100644 --- a/app/src/main/java/to/bitkit/repositories/WidgetsRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WidgetsRepo.kt @@ -84,7 +84,7 @@ class WidgetsRepo @Inject constructor( private fun updateWidgetJobs(enabledWidgetTypes: Set) { val widgetTypesWithServices = WidgetType.entries.filter { - it != WidgetType.CALCULATOR + it != WidgetType.CALCULATOR && it != WidgetType.SUGGESTIONS } widgetTypesWithServices.forEach { widgetType -> @@ -138,7 +138,9 @@ class WidgetsRepo @Inject constructor( } } - WidgetType.CALCULATOR -> throw NotImplementedError("Calculator widget doesn't need a service") + WidgetType.CALCULATOR, + WidgetType.SUGGESTIONS, + -> throw NotImplementedError("Widget doesn't need a service") } widgetJobs[widgetType] = job @@ -226,8 +228,10 @@ class WidgetsRepo @Inject constructor( widgetsStore.updateBlock(block) } - WidgetType.CALCULATOR -> { - throw NotImplementedError("Calculator widget doesn't need a service") + WidgetType.CALCULATOR, + WidgetType.SUGGESTIONS, + -> { + throw NotImplementedError("Widget doesn't need a service") } WidgetType.FACTS -> updateWidget(factsService) { facts -> diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 4c5ca717b..435f7844f 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -105,6 +105,8 @@ import to.bitkit.ui.screens.wallets.activity.TagSelectorSheet import to.bitkit.ui.screens.wallets.receive.ReceiveSheet import to.bitkit.ui.screens.wallets.suggestion.BuyIntroScreen import to.bitkit.ui.screens.widgets.AddWidgetsScreen +import to.bitkit.ui.screens.widgets.suggestions.SuggestionsPreviewScreen +import to.bitkit.ui.screens.widgets.suggestions.SuggestionsViewModel import to.bitkit.ui.screens.widgets.WidgetsIntroScreen import to.bitkit.ui.screens.widgets.blocks.BlocksEditScreen import to.bitkit.ui.screens.widgets.blocks.BlocksPreviewScreen @@ -1349,6 +1351,7 @@ private fun NavGraphBuilder.widgets( ) } composableWithDefaultTransitions { + val showWidgets by settingsViewModel.showWidgets.collectAsStateWithLifecycle() AddWidgetsScreen( onWidgetSelected = { widgetType -> when (widgetType) { @@ -1358,10 +1361,21 @@ private fun NavGraphBuilder.widgets( WidgetType.NEWS -> navController.navigate(Routes.HeadlinesPreview) WidgetType.PRICE -> navController.navigate(Routes.PricePreview) WidgetType.WEATHER -> navController.navigate(Routes.WeatherPreview) + WidgetType.SUGGESTIONS -> navController.navigate(Routes.SuggestionsPreview) } }, fiatSymbol = LocalCurrencies.current.currencySymbol, - onBackCLick = { navController.popBackStack() } + onBackCLick = { navController.popBackStack() }, + showWidgets = showWidgets, + onEnableInSettingsClick = { navController.navigate(Routes.WidgetsSettings) }, + ) + } + composableWithDefaultTransitions { + val viewModel = hiltViewModel() + SuggestionsPreviewScreen( + suggestionsViewModel = viewModel, + onClose = { navController.navigateToHome() }, + onBack = { navController.popBackStack() }, ) } composableWithDefaultTransitions { @@ -1931,6 +1945,9 @@ sealed interface Routes { @Serializable data object AddWidget : Routes + @Serializable + data object SuggestionsPreview : Routes + @Serializable data object Headlines : Routes diff --git a/app/src/main/java/to/bitkit/ui/components/ActivityBanner.kt b/app/src/main/java/to/bitkit/ui/components/ActivityBanner.kt index 5d03ad156..43e3aa849 100644 --- a/app/src/main/java/to/bitkit/ui/components/ActivityBanner.kt +++ b/app/src/main/java/to/bitkit/ui/components/ActivityBanner.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Icon @@ -161,7 +162,8 @@ fun ActivityBanner( Icon( painter = painterResource(icon), contentDescription = null, - tint = gradientColor + tint = gradientColor, + modifier = Modifier.size(20.dp) ) Headline20( diff --git a/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt b/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt index fe8505b24..e67ae0768 100644 --- a/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt +++ b/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R @@ -203,6 +204,7 @@ fun LargeRow( if (!hideBalance && prefix != null) { Display( text = prefix, + fontWeight = FontWeight.ExtraBold, color = Colors.White64, modifier = Modifier .padding(end = 8.dp) @@ -212,6 +214,7 @@ fun LargeRow( if (showSymbol && !isSymbolSuffix) { Display( text = symbol, + fontWeight = FontWeight.ExtraBold, color = Colors.White64, modifier = Modifier .padding(end = 8.dp) @@ -231,6 +234,7 @@ fun LargeRow( if (showSymbol && isSymbolSuffix) { Display( text = symbol, + fontWeight = FontWeight.ExtraBold, color = Colors.White64, modifier = Modifier .padding(start = 8.dp) diff --git a/app/src/main/java/to/bitkit/ui/components/SuggestionCard.kt b/app/src/main/java/to/bitkit/ui/components/SuggestionCard.kt index 8c1a36fb8..556197568 100644 --- a/app/src/main/java/to/bitkit/ui/components/SuggestionCard.kt +++ b/app/src/main/java/to/bitkit/ui/components/SuggestionCard.kt @@ -12,7 +12,8 @@ import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -25,6 +26,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.ShapeDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush @@ -54,7 +56,6 @@ fun SuggestionCard( description: String, @DrawableRes icon: Int, onClose: (() -> Unit)? = null, - size: Int = 152, disableGlow: Boolean = false, captionColor: Color = Colors.White64, onClick: () -> Unit, @@ -75,7 +76,8 @@ fun SuggestionCard( Box( modifier = modifier - .size(size.dp) + .fillMaxWidth() + .aspectRatio(1f) .clip(ShapeDefaults.Large) .then( if (isDismissible || disableGlow) { @@ -108,12 +110,12 @@ fun SuggestionCard( .clickableAlpha { onClick() } ) { Column( + verticalArrangement = Arrangement.Bottom, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + .padding(16.dp), ) { - Row( + Box( modifier = Modifier .fillMaxWidth() .weight(1f) @@ -121,8 +123,11 @@ fun SuggestionCard( Image( painter = painterResource(icon), contentDescription = null, - contentScale = ContentScale.FillHeight, - modifier = Modifier.weight(1f) + alignment = Alignment.TopStart, + contentScale = ContentScale.Fit, + modifier = Modifier + .defaultMinSize(minHeight = 80.dp) + .align(Alignment.BottomStart) ) if (onClose != null) { @@ -130,12 +135,13 @@ fun SuggestionCard( onClick = onClose, modifier = Modifier .size(16.dp) + .align(Alignment.TopEnd) .testTag("SuggestionDismiss") ) { Icon( painter = painterResource(R.drawable.ic_x), contentDescription = null, - tint = Colors.White, + tint = Colors.White64, ) } } @@ -144,6 +150,7 @@ fun SuggestionCard( Headline20( text = AnnotatedString(title), color = Colors.White, + modifier = Modifier.padding(top = 4.dp), ) CaptionB( @@ -173,7 +180,8 @@ private fun Modifier.gradientRadialBackground( @Composable private fun Preview() { LazyVerticalGrid( - verticalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), columns = GridCells.Fixed(2), modifier = Modifier.fillMaxSize(), ) { diff --git a/app/src/main/java/to/bitkit/ui/components/TabBar.kt b/app/src/main/java/to/bitkit/ui/components/TabBar.kt index fd629f34e..4a305e789 100644 --- a/app/src/main/java/to/bitkit/ui/components/TabBar.kt +++ b/app/src/main/java/to/bitkit/ui/components/TabBar.kt @@ -50,6 +50,9 @@ import to.bitkit.ui.theme.Colors private val iconToTextGap = 4.dp private val iconSize = 20.dp +const val TAB_BAR_HEIGHT = 56 +const val TAB_BAR_PADDING_BOTTOM = 8 + private val buttonLeftShape = RoundedCornerShape(topStartPercent = 50, bottomStartPercent = 50) private val buttonRightShape = RoundedCornerShape(topEndPercent = 50, bottomEndPercent = 50) @@ -67,7 +70,7 @@ fun BoxScope.TabBar( .align(Alignment.BottomCenter) .fillMaxWidth() .padding(horizontal = 16.dp) - .padding(bottom = 16.dp) + .padding(bottom = TAB_BAR_PADDING_BOTTOM.dp) .navigationBarsPadding() ) { Row( @@ -81,7 +84,7 @@ fun BoxScope.TabBar( contentAlignment = Alignment.Center, modifier = Modifier .weight(1f) - .height(60.dp) + .height(TAB_BAR_HEIGHT.dp) .clip(buttonLeftShape) .clickable { onSendClick() } .testTag("Send") @@ -102,7 +105,7 @@ fun BoxScope.TabBar( contentAlignment = Alignment.Center, modifier = Modifier .weight(1f) - .height(60.dp) + .height(TAB_BAR_HEIGHT.dp) .clip(buttonRightShape) .clickable { onReceiveClick() } .testTag("Receive") diff --git a/app/src/main/java/to/bitkit/ui/components/Text.kt b/app/src/main/java/to/bitkit/ui/components/Text.kt index 8bfa181b4..e30ad3b2d 100644 --- a/app/src/main/java/to/bitkit/ui/components/Text.kt +++ b/app/src/main/java/to/bitkit/ui/components/Text.kt @@ -84,6 +84,23 @@ fun Headline20( ) } +@Composable +fun Headline24( + text: AnnotatedString, + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colorScheme.primary, +) { + Text( + text = text.toUpperCase(), + style = AppTextStyles.Headline.merge( + fontSize = 24.sp, + lineHeight = 24.sp, + color = color, + ), + modifier = modifier, + ) +} + @Composable fun Title( text: String, diff --git a/app/src/main/java/to/bitkit/ui/components/WalletBalanceView.kt b/app/src/main/java/to/bitkit/ui/components/WalletBalanceView.kt index f303de52d..1183609ef 100644 --- a/app/src/main/java/to/bitkit/ui/components/WalletBalanceView.kt +++ b/app/src/main/java/to/bitkit/ui/components/WalletBalanceView.kt @@ -106,7 +106,7 @@ private fun RowScope.Content( text = title, color = Colors.White64, ) - Spacer(modifier = Modifier.height(8.dp)) + VerticalSpacer(8.dp) converted?.let { converted -> Row( diff --git a/app/src/main/java/to/bitkit/ui/screens/shop/shopDiscover/ShopDiscoverScreen.kt b/app/src/main/java/to/bitkit/ui/screens/shop/shopDiscover/ShopDiscoverScreen.kt index 8517a76eb..b9fc346c8 100644 --- a/app/src/main/java/to/bitkit/ui/screens/shop/shopDiscover/ShopDiscoverScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/shop/shopDiscover/ShopDiscoverScreen.kt @@ -55,8 +55,6 @@ import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.theme.Shapes -private const val SHOP_CARD_SIZE = 164 - @OptIn(ExperimentalMaterial3Api::class) @Composable fun ShopDiscoverScreen( @@ -128,7 +126,7 @@ private fun ShopTabContent( description = stringResource(R.string.other__shop__discover__gift_cards__description), icon = R.drawable.gift, captionColor = Colors.Gray1, - size = SHOP_CARD_SIZE, + disableGlow = true, onClick = { navigateWebView("gift-cards", title) @@ -142,7 +140,7 @@ private fun ShopTabContent( description = stringResource(R.string.other__shop__discover__esims__description), icon = R.drawable.globe, captionColor = Colors.Gray1, - size = SHOP_CARD_SIZE, + disableGlow = true, onClick = { navigateWebView("esims", title2) @@ -163,7 +161,7 @@ private fun ShopTabContent( description = stringResource(R.string.other__shop__discover__refill__description), icon = R.drawable.phone, captionColor = Colors.Gray1, - size = SHOP_CARD_SIZE, + disableGlow = true, onClick = { navigateWebView("refill", title) @@ -176,7 +174,7 @@ private fun ShopTabContent( title = title2, description = stringResource(R.string.other__shop__discover__travel__description), icon = R.drawable.rocket_2, - size = SHOP_CARD_SIZE, + disableGlow = true, captionColor = Colors.Gray1, onClick = { diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt index 1543b821d..1b523297f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt @@ -1,15 +1,15 @@ package to.bitkit.ui.screens.wallets import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.snapping.SnapPosition -import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -17,9 +17,12 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.pager.PagerDefaults +import androidx.compose.foundation.pager.VerticalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.DrawerState @@ -44,6 +47,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource @@ -55,34 +59,44 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController import com.synonym.bitkitcore.Activity import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.rememberHazeState -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import to.bitkit.R +import to.bitkit.data.dto.price.Change +import to.bitkit.data.dto.price.GraphPeriod +import to.bitkit.data.dto.price.PriceDTO +import to.bitkit.data.dto.price.PriceWidgetData +import to.bitkit.data.dto.price.TradingPair import to.bitkit.env.Env import to.bitkit.models.ActivityBannerType import to.bitkit.models.BalanceState +import to.bitkit.models.BannerItem import to.bitkit.models.Suggestion import to.bitkit.models.WidgetType +import to.bitkit.models.WidgetWithPosition +import to.bitkit.models.widget.ArticleModel +import to.bitkit.models.widget.BlockModel import to.bitkit.ui.LocalBalances import to.bitkit.ui.Routes import to.bitkit.ui.components.ActivityBanner import to.bitkit.ui.components.AppStatus import to.bitkit.ui.components.BalanceHeaderView import to.bitkit.ui.components.EmptyStateView +import to.bitkit.ui.components.FillHeight +import to.bitkit.ui.components.Headline24 import to.bitkit.ui.components.HorizontalSpacer import to.bitkit.ui.components.Sheet import to.bitkit.ui.components.StatusBarSpacer import to.bitkit.ui.components.SuggestionCard +import to.bitkit.ui.components.TAB_BAR_HEIGHT +import to.bitkit.ui.components.TAB_BAR_PADDING_BOTTOM import to.bitkit.ui.components.TabBar import to.bitkit.ui.components.TertiaryButton -import to.bitkit.ui.components.Text13Up import to.bitkit.ui.components.TopBarSpacer import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.WalletBalanceView @@ -97,6 +111,7 @@ import to.bitkit.ui.screens.wallets.activity.utils.previewActivityItems import to.bitkit.ui.screens.widgets.DragAndDropWidget import to.bitkit.ui.screens.widgets.DragDropColumn import to.bitkit.ui.screens.widgets.blocks.BlockCard +import to.bitkit.ui.screens.widgets.blocks.WeatherModel import to.bitkit.ui.screens.widgets.calculator.components.CalculatorCard import to.bitkit.ui.screens.widgets.facts.FactsCard import to.bitkit.ui.screens.widgets.headlines.HeadlineCard @@ -108,12 +123,18 @@ import to.bitkit.ui.sheets.BackupRoute import to.bitkit.ui.sheets.PinRoute import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.theme.Insets import to.bitkit.ui.utils.withAccent import to.bitkit.viewmodels.ActivityListViewModel import to.bitkit.viewmodels.AppViewModel import to.bitkit.viewmodels.SettingsViewModel import to.bitkit.viewmodels.WalletViewModel +private const val SMALL_SCREEN_HEIGHT_DP = 700 +private const val SMALL_SCREEN_ACTIVITY_COUNT = 2 +private const val LARGE_SCREEN_ACTIVITY_COUNT = 3 +private const val ANIMATION_DURATION_MS = 300 + @Suppress("CyclomaticComplexMethod") @Composable fun HomeScreen( @@ -130,7 +151,6 @@ fun HomeScreen( val context = LocalContext.current val hasSeenTransferIntro by settingsViewModel.hasSeenTransferIntro.collectAsStateWithLifecycle() val hasSeenShopIntro by settingsViewModel.hasSeenShopIntro.collectAsStateWithLifecycle() - val hasSeenProfileIntro by settingsViewModel.hasSeenProfileIntro.collectAsStateWithLifecycle() val hasSeenWidgetsIntro: Boolean by settingsViewModel.hasSeenWidgetsIntro.collectAsStateWithLifecycle() val bgPaymentsIntroSeen: Boolean by settingsViewModel.bgPaymentsIntroSeen.collectAsStateWithLifecycle() val quickPayIntroSeen by settingsViewModel.quickPayIntroSeen.collectAsStateWithLifecycle() @@ -155,8 +175,6 @@ fun HomeScreen( Content( isRefreshing = isRefreshing, homeUiState = homeUiState, - rootNavController = rootNavController, - walletNavController = walletNavController, drawerState = drawerState, latestActivities = latestActivities, onRefresh = { @@ -232,6 +250,7 @@ fun HomeScreen( } }, onClickAddWidget = { + homeViewModel.disableEditMode() if (!hasSeenWidgetsIntro) { rootNavController.navigate(Routes.WidgetsIntro) } else { @@ -248,6 +267,7 @@ fun HomeScreen( WidgetType.NEWS -> rootNavController.navigate(Routes.HeadlinesPreview) WidgetType.PRICE -> rootNavController.navigate(Routes.PricePreview) WidgetType.WEATHER -> rootNavController.navigate(Routes.WeatherPreview) + WidgetType.SUGGESTIONS -> rootNavController.navigate(Routes.SuggestionsPreview) } }, onClickDeleteWidget = { widgetType -> @@ -256,8 +276,14 @@ fun HomeScreen( onMoveWidget = { fromIndex, toIndex -> homeViewModel.moveWidget(fromIndex, toIndex) }, - onDismissEmptyState = homeViewModel::dismissEmptyState, - onClickEmptyActivityRow = { appViewModel.showSheet(Sheet.Receive) }, + onPageChanged = homeViewModel::onPageChanged, + onDismissWidgetsOnboardingHint = homeViewModel::dismissWidgetsOnboardingHint, + onNavigateToAppStatus = { rootNavController.navigate(Routes.AppStatus) }, + onNavigateToSettingUp = { rootNavController.navigate(Routes.SettingUp) }, + onNavigateToAllActivity = { rootNavController.navigateToAllActivity() }, + onNavigateToActivityItem = { rootNavController.navigateToActivityItem(it) }, + onNavigateToSavings = { walletNavController.navigate(Routes.Savings) }, + onNavigateToSpending = { walletNavController.navigate(Routes.Spending) }, ) } @@ -267,10 +293,7 @@ fun HomeScreen( private fun Content( isRefreshing: Boolean, homeUiState: HomeUiState, - rootNavController: NavController, - walletNavController: NavController, drawerState: DrawerState, - hazeState: HazeState = rememberHazeState(), latestActivities: List?, onRefresh: () -> Unit = {}, onRemoveSuggestion: (Suggestion) -> Unit = {}, @@ -280,192 +303,153 @@ private fun Content( onClickEditWidget: (WidgetType) -> Unit = {}, onClickDeleteWidget: (WidgetType) -> Unit = {}, onMoveWidget: (Int, Int) -> Unit = { _, _ -> }, - onDismissEmptyState: () -> Unit = {}, - onClickEmptyActivityRow: () -> Unit = {}, + onPageChanged: (Int) -> Unit = {}, + onDismissWidgetsOnboardingHint: () -> Unit = {}, + onNavigateToAppStatus: () -> Unit = {}, + onNavigateToSettingUp: () -> Unit = {}, + onNavigateToAllActivity: () -> Unit = {}, + onNavigateToActivityItem: (String) -> Unit = {}, + onNavigateToSavings: () -> Unit = {}, + onNavigateToSpending: () -> Unit = {}, + hazeState: HazeState = rememberHazeState(), balances: BalanceState = LocalBalances.current, ) { val scope = rememberCoroutineScope() + val pageCount = if (homeUiState.showWidgets) 2 else 1 + val pagerState = rememberPagerState( + initialPage = homeUiState.currentPage, + pageCount = { pageCount }, + ) + + LaunchedEffect(pagerState.currentPage) { + onPageChanged(pagerState.currentPage) + if (pagerState.currentPage == 1 && !latestActivities.isNullOrEmpty()) { + onDismissWidgetsOnboardingHint() + } + } + + val screenHeightDp = LocalConfiguration.current.screenHeightDp + val activityCount = if (screenHeightDp < SMALL_SCREEN_HEIGHT_DP) { + SMALL_SCREEN_ACTIVITY_COUNT + } else { + LARGE_SCREEN_ACTIVITY_COUNT + } Box { - val heightStatusBar = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() TopBar( hazeState = hazeState, - rootNavController = rootNavController, - scope = scope, - drawerState = drawerState, + showEditWidgets = homeUiState.currentPage == 1 && homeUiState.showWidgets, + isEditingWidgets = homeUiState.isEditingWidgets, + onClickEditWidgetList = onClickEditWidgetList, + onNavigateToAppStatus = onNavigateToAppStatus, + onOpenDrawer = { scope.launch { drawerState.open() } }, ) - val pullToRefreshState = rememberPullToRefreshState() - PullToRefreshBox( - state = pullToRefreshState, - isRefreshing = isRefreshing, - onRefresh = onRefresh, - indicator = { - Indicator( - isRefreshing = isRefreshing, - state = pullToRefreshState, - modifier = Modifier - .padding(top = heightStatusBar) - .align(Alignment.TopCenter) - ) - }, + + VerticalPager( + state = pagerState, + userScrollEnabled = homeUiState.showWidgets, + flingBehavior = PagerDefaults.flingBehavior( + state = pagerState, + snapPositionalThreshold = 0.2f, + ), modifier = Modifier .fillMaxSize() .hazeSource(state = hazeState) .zIndex(0f) - ) { - Column( - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .testTag("HomeScrollView") - ) { - StatusBarSpacer() - TopBarSpacer() - VerticalSpacer(16.dp) - BalanceHeaderView( - sats = balances.totalSats.toLong(), - showEyeIcon = true, - testTag = "TotalBalance", - modifier = Modifier - .fillMaxWidth() - .testTag("TotalBalance") + ) { page -> + when (page) { + 0 -> WalletPage( + isRefreshing = isRefreshing, + homeUiState = homeUiState, + latestActivities = latestActivities?.take(activityCount), + balances = balances, + onRefresh = onRefresh, + onNavigateToSettingUp = onNavigateToSettingUp, + onNavigateToAllActivity = onNavigateToAllActivity, + onNavigateToActivityItem = onNavigateToActivityItem, + onNavigateToSavings = onNavigateToSavings, + onNavigateToSpending = onNavigateToSpending, ) - if (!homeUiState.showEmptyState) { - Spacer(modifier = Modifier.height(32.dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .height(IntrinsicSize.Min) - ) { - WalletBalanceView( - title = stringResource(R.string.wallet__savings__title), - sats = balances.totalOnchainSats.toLong(), - icon = painterResource(id = R.drawable.ic_btc_circle), - modifier = Modifier - .clickableAlpha { walletNavController.navigate(Routes.Savings) } - .padding(vertical = 4.dp) - .testTag("ActivitySavings") - ) - VerticalDivider() - HorizontalSpacer(16.dp) - WalletBalanceView( - title = stringResource(R.string.wallet__spending__title), - sats = balances.totalLightningSats.toLong(), - icon = painterResource(id = R.drawable.ic_ln_circle), - modifier = Modifier - .clickableAlpha { walletNavController.navigate(Routes.Spending) } - .padding(vertical = 4.dp) - .testTag("ActivitySpending") - ) - } - AnimatedVisibility(homeUiState.suggestions.isNotEmpty()) { - val state = rememberLazyListState() - val snapBehavior = rememberSnapFlingBehavior( - lazyListState = state, - snapPosition = SnapPosition.Start - ) - - Column { - Spacer(modifier = Modifier.height(32.dp)) - Text13Up(stringResource(R.string.cards__suggestions), color = Colors.White64) - Spacer(modifier = Modifier.height(16.dp)) - LazyRow( - horizontalArrangement = Arrangement.spacedBy(16.dp), - state = state, - flingBehavior = snapBehavior, - modifier = Modifier - .fillMaxWidth() - .testTag("Suggestions") - ) { - items(homeUiState.suggestions, key = { it.name }) { item -> - SuggestionCard( - gradientColor = item.color, - title = stringResource(item.title), - description = stringResource(item.description), - icon = item.icon, - onClose = { onRemoveSuggestion(item) }.takeIf { item.dismissible }, - onClick = { onClickSuggestion(item) }, - modifier = Modifier.testTag("Suggestion-${item.name.lowercase()}") - ) - } - } - } - } + 1 -> WidgetsPage( + homeUiState = homeUiState, + onRemoveSuggestion = onRemoveSuggestion, + onClickSuggestion = onClickSuggestion, + onClickAddWidget = onClickAddWidget, + onClickEditWidget = onClickEditWidget, + onClickDeleteWidget = onClickDeleteWidget, + onMoveWidget = onMoveWidget, + ) + } + } + } +} - if (homeUiState.showWidgets) { - Spacer(modifier = Modifier.height(32.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text13Up( - stringResource(R.string.widgets__widgets), - color = Colors.White64 - ) - - IconButton( - onClick = onClickEditWidgetList, - modifier = Modifier.testTag("WidgetsEdit") - ) { - Icon( - painter = when (homeUiState.isEditingWidgets) { - true -> painterResource(R.drawable.ic_check) - else -> painterResource(R.drawable.ic_sort_ascending) - }, - contentDescription = null, - ) - } - } +@Suppress("MagicNumber") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun WalletPage( + isRefreshing: Boolean, + homeUiState: HomeUiState, + latestActivities: List?, + balances: BalanceState, + onRefresh: () -> Unit, + onNavigateToSettingUp: () -> Unit, + onNavigateToAllActivity: () -> Unit, + onNavigateToActivityItem: (String) -> Unit, + onNavigateToSavings: () -> Unit, + onNavigateToSpending: () -> Unit, +) { + val heightStatusBar = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + val pullToRefreshState = rememberPullToRefreshState() + val hasActivity = !latestActivities.isNullOrEmpty() - Spacer(modifier = Modifier.height(16.dp)) - - if (homeUiState.isEditingWidgets) { - DragDropColumn( - items = homeUiState.widgetsWithPosition, - onMove = onMoveWidget, - modifier = Modifier.fillMaxWidth() - ) { widgetWithPosition, isDragging -> - DragAndDropWidget( - iconRes = widgetWithPosition.type.iconRes, - title = stringResource(widgetWithPosition.type.title), - onClickSettings = { onClickEditWidget(widgetWithPosition.type) }, - onClickDelete = { onClickDeleteWidget(widgetWithPosition.type) }, - modifier = Modifier - .fillMaxWidth() - .graphicsLayer { - alpha = if (isDragging) 0.8f else 1.0f - } - ) - } - } else { - Widgets(homeUiState) - } + PullToRefreshBox( + state = pullToRefreshState, + isRefreshing = isRefreshing, + onRefresh = onRefresh, + indicator = { + Indicator( + isRefreshing = isRefreshing, + state = pullToRefreshState, + modifier = Modifier + .padding(top = heightStatusBar) + .align(Alignment.TopCenter) + ) + }, + modifier = Modifier.fillMaxSize() + ) { + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .testTag("HomeScrollView") + ) { + StatusBarSpacer() + TopBarSpacer() + VerticalSpacer(16.dp) - Spacer(modifier = Modifier.height(32.dp)) - TertiaryButton( - text = stringResource(R.string.widgets__add), - icon = { - Icon( - painter = painterResource(R.drawable.ic_plus), - contentDescription = null, - tint = Colors.White80, - ) - }, - onClick = onClickAddWidget, - modifier = Modifier.testTag("WidgetsAdd") - ) - } - Spacer(modifier = Modifier.height(32.dp)) + BalanceHeaderView( + sats = balances.totalSats.toLong(), + showEyeIcon = true, + testTag = "TotalBalance", + modifier = Modifier + .fillMaxWidth() + .testTag("TotalBalance") + ) + VerticalSpacer(32.dp) + BalancesSection(balances, onNavigateToSavings, onNavigateToSpending) + VerticalSpacer(32.dp) + if (!homeUiState.showEmptyState) { + if (hasActivity) { AnimatedVisibility(homeUiState.banners.isNotEmpty()) { Column( verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier .fillMaxWidth() - .padding(bottom = 18.dp) + .padding(bottom = 16.dp) ) { homeUiState.banners.forEach { banner -> ActivityBanner( @@ -474,7 +458,7 @@ private fun Content( icon = banner.type.icon, onClick = { when (banner.type) { - ActivityBannerType.SPENDING -> rootNavController.navigate(Routes.SettingUp) + ActivityBannerType.SPENDING -> onNavigateToSettingUp() ActivityBannerType.SAVINGS -> Unit } }, @@ -486,29 +470,203 @@ private fun Content( ActivityListSimple( items = latestActivities, - onAllActivityClick = { rootNavController.navigateToAllActivity() }, - onActivityItemClick = { rootNavController.navigateToActivityItem(it) }, - onEmptyActivityRowClick = onClickEmptyActivityRow, + onAllActivityClick = onNavigateToAllActivity, + onActivityItemClick = onNavigateToActivityItem, ) - VerticalSpacer(150.dp) // scrollable empty space behind footer + FillHeight() + + if (homeUiState.showWidgetsOnboardingHint) { + WidgetsOnboardingHint() + } } + + VerticalSpacer(TAB_BAR_HEIGHT.dp + TAB_BAR_PADDING_BOTTOM.dp + 36.dp + Insets.Bottom) } - if (homeUiState.showEmptyState) { - EmptyStateView( - text = stringResource(R.string.onboarding__empty_wallet).withAccent(), - onClose = onDismissEmptyState, + } + + if (homeUiState.showEmptyState) { + EmptyStateView( + text = stringResource(R.string.onboarding__empty_wallet).withAccent(), + modifier = Modifier + .align(Alignment.BottomCenter) + ) + } + } +} + +@Composable +fun BalancesSection( + balances: BalanceState, + onNavigateToSavings: () -> Unit, + onNavigateToSpending: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + ) { + WalletBalanceView( + title = stringResource(R.string.wallet__savings__title), + sats = balances.totalOnchainSats.toLong(), + icon = painterResource(id = R.drawable.ic_btc_circle), + modifier = Modifier + .clickableAlpha(onClick = onNavigateToSavings) + .padding(vertical = 4.dp) + .testTag("ActivitySavings") + ) + VerticalDivider(color = Colors.Gray4) + HorizontalSpacer(16.dp) + WalletBalanceView( + title = stringResource(R.string.wallet__spending__title), + sats = balances.totalLightningSats.toLong(), + icon = painterResource(id = R.drawable.ic_ln_circle), + modifier = Modifier + .clickableAlpha(onClick = onNavigateToSpending) + .padding(vertical = 4.dp) + .testTag("ActivitySpending") + ) + } +} + +@Suppress("MagicNumber") +@Composable +private fun WidgetsPage( + homeUiState: HomeUiState, + onRemoveSuggestion: (Suggestion) -> Unit, + onClickSuggestion: (Suggestion) -> Unit, + onClickAddWidget: () -> Unit, + onClickEditWidget: (WidgetType) -> Unit, + onClickDeleteWidget: (WidgetType) -> Unit, + onMoveWidget: (Int, Int) -> Unit, +) { + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + StatusBarSpacer() + TopBarSpacer() + VerticalSpacer(16.dp) + + if (homeUiState.isEditingWidgets) { + DragDropColumn( + items = homeUiState.widgetsWithPosition, + onMove = onMoveWidget, + modifier = Modifier.fillMaxWidth() + ) { widgetWithPosition, isDragging -> + DragAndDropWidget( + iconRes = widgetWithPosition.type.iconRes, + title = stringResource(widgetWithPosition.type.title), + onClickSettings = { onClickEditWidget(widgetWithPosition.type) }, + onClickDelete = { onClickDeleteWidget(widgetWithPosition.type) }, modifier = Modifier - .align(Alignment.BottomCenter) - .padding(bottom = 24.dp) + .fillMaxWidth() + .graphicsLayer { + alpha = if (isDragging) 0.8f else 1.0f + } ) } + } else { + Widgets( + homeUiState = homeUiState, + onRemoveSuggestion = onRemoveSuggestion, + onClickSuggestion = onClickSuggestion, + ) } + + VerticalSpacer(16.dp) + + TertiaryButton( + text = stringResource(R.string.widgets__add), + icon = { + Icon( + painter = painterResource(R.drawable.ic_plus), + contentDescription = null, + tint = Colors.White80, + ) + }, + onClick = onClickAddWidget, + modifier = Modifier.testTag("WidgetsAdd") + ) + + VerticalSpacer(150.dp) } } @Composable -private fun Widgets(homeUiState: HomeUiState) { +private fun SuggestionsSection( + suggestions: List, + onRemoveSuggestion: (Suggestion) -> Unit, + onClickSuggestion: (Suggestion) -> Unit, + modifier: Modifier = Modifier, +) { + val rows = (suggestions.size + 1) / 2 + + BoxWithConstraints(modifier = modifier.fillMaxWidth()) { + val cardSize = (maxWidth - 16.dp) / 2 + val gridHeight = (cardSize * rows) + (16.dp * (rows - 1)) + LazyVerticalGrid( + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + userScrollEnabled = false, + modifier = Modifier + .fillMaxWidth() + .height(gridHeight) + .testTag("Suggestions") + ) { + items( + items = suggestions, + key = { it.name } + ) { item -> + SuggestionCard( + gradientColor = item.color, + title = stringResource(item.title), + description = stringResource(item.description), + icon = item.icon, + onClose = { onRemoveSuggestion(item) }.takeIf { item.dismissible }, + onClick = { onClickSuggestion(item) }, + modifier = Modifier + .testTag("Suggestion-${item.name.lowercase()}") + .animateItem( + fadeInSpec = tween(durationMillis = ANIMATION_DURATION_MS), + fadeOutSpec = tween(durationMillis = ANIMATION_DURATION_MS), + placementSpec = tween(durationMillis = ANIMATION_DURATION_MS), + ) + ) + } + } + } +} + +@Composable +private fun WidgetsOnboardingHint(modifier: Modifier = Modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .padding(top = 32.dp) + ) { + Headline24( + text = stringResource(R.string.widgets__onboarding_swipe).withAccent(), + modifier = Modifier.weight(1f) + ) + Image( + painter = painterResource(R.drawable.swipe_instruction), + contentDescription = null, + ) + } +} + +@Suppress("CyclomaticComplexMethod") +@Composable +private fun Widgets( + homeUiState: HomeUiState, + onRemoveSuggestion: (Suggestion) -> Unit, + onClickSuggestion: (Suggestion) -> Unit, +) { Column( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp) @@ -599,6 +757,16 @@ private fun Widgets(homeUiState: HomeUiState) { ) } } + + WidgetType.SUGGESTIONS -> { + if (homeUiState.suggestions.isNotEmpty()) { + SuggestionsSection( + suggestions = homeUiState.suggestions, + onRemoveSuggestion = onRemoveSuggestion, + onClickSuggestion = onClickSuggestion, + ) + } + } } } } @@ -608,9 +776,11 @@ private fun Widgets(homeUiState: HomeUiState) { @OptIn(ExperimentalMaterial3Api::class) private fun TopBar( hazeState: HazeState, - rootNavController: NavController, - scope: CoroutineScope, - drawerState: DrawerState, + showEditWidgets: Boolean = false, + isEditingWidgets: Boolean = false, + onClickEditWidgetList: () -> Unit = {}, + onNavigateToAppStatus: () -> Unit = {}, + onOpenDrawer: () -> Unit = {}, ) { val topbarGradient = Brush.verticalGradient( colorStops = arrayOf( @@ -631,14 +801,32 @@ private fun TopBar( TopAppBar( title = {}, actions = { - AppStatus(onClick = { rootNavController.navigate(Routes.AppStatus) }) - HorizontalSpacer(4.dp) + AnimatedVisibility(showEditWidgets) { + IconButton( + onClick = onClickEditWidgetList, + modifier = Modifier.testTag("WidgetsEdit") + ) { + Icon( + painter = if (isEditingWidgets) { + painterResource(R.drawable.ic_check) + } else { + painterResource(R.drawable.ic_edit) + }, + tint = Colors.White, + contentDescription = null, + ) + } + } + AppStatus( + onClick = onNavigateToAppStatus, + ) IconButton( - onClick = { scope.launch { drawerState.open() } }, + onClick = onOpenDrawer, modifier = Modifier.testTag("HeaderMenu") ) { Icon( painter = painterResource(R.drawable.ic_list), + tint = Colors.White, contentDescription = stringResource(R.string.settings__settings), ) } @@ -667,9 +855,58 @@ private fun DeleteWidgetAlert( ) } -@Preview(showSystemUi = true) +private val previewBalances = BalanceState( + totalOnchainSats = 165_000u, + totalLightningSats = 45_000u, +) + +private val previewWidgets = listOf( + WidgetWithPosition(type = WidgetType.SUGGESTIONS, position = 0), + WidgetWithPosition(type = WidgetType.PRICE, position = 1), + WidgetWithPosition(type = WidgetType.BLOCK, position = 2), + WidgetWithPosition(type = WidgetType.NEWS, position = 3), +) + +private val previewBlock = BlockModel( + height = "761,405", + time = "01:31:42 UTC", + date = "01/2/2022", + transactionCount = "2,175", + size = "1,606kB", + source = "mempool.io", +) + +private val previewArticle = ArticleModel( + timeAgo = "21 minutes ago", + title = "How Bitcoin changed El Salvador in more ways", + publisher = "bitcoinmagazine.com", + link = "bitcoinmagazine.com", +) + +private val previewPrice = PriceDTO( + source = "Bitfinex.com", + widgets = listOf( + PriceWidgetData( + pair = TradingPair.BTC_USD, + change = Change(isPositive = true, formatted = "$ 20,326"), + price = "$20,326", + pastValues = listOf(1.0, 2.0, 3.0, 4.0), + period = GraphPeriod.ONE_DAY, + ), + ), +) + +private val previewWeather = WeatherModel( + title = R.string.widgets__weather__condition__good__title, + description = R.string.widgets__weather__condition__good__description, + currentFee = "15 sat/vB", + nextBlockFee = "12 sat/vB", + icon = "\u2600\uFE0F", +) + +@Preview(name = "With Activity", showSystemUi = true) @Composable -private fun Preview() { +private fun PreviewWithActivity() { AppThemeSurface { Box { Content( @@ -677,21 +914,16 @@ private fun Preview() { homeUiState = HomeUiState( showWidgets = true, ), - rootNavController = rememberNavController(), - walletNavController = rememberNavController(), drawerState = rememberDrawerState(initialValue = DrawerValue.Closed), latestActivities = previewActivityItems.take(3), - balances = BalanceState( - totalOnchainSats = 165_000u, - totalLightningSats = 45_000u, - ), + balances = previewBalances, ) TabBar() } } } -@Preview(showSystemUi = true) +@Preview(name = "Empty", showSystemUi = true) @Composable private fun PreviewEmpty() { AppThemeSurface { @@ -701,11 +933,101 @@ private fun PreviewEmpty() { homeUiState = HomeUiState( showEmptyState = true, ), - rootNavController = rememberNavController(), - walletNavController = rememberNavController(), drawerState = rememberDrawerState(initialValue = DrawerValue.Closed), latestActivities = previewActivityItems.take(3), - balances = BalanceState() + balances = BalanceState(), + ) + TabBar() + } + } +} + +@Preview(name = "With Banners", showSystemUi = true) +@Composable +private fun PreviewWithBanners() { + AppThemeSurface { + Box { + Content( + isRefreshing = false, + homeUiState = HomeUiState( + showWidgets = true, + banners = ActivityBannerType.entries.map { BannerItem(type = it, title = "") }, + ), + drawerState = rememberDrawerState(initialValue = DrawerValue.Closed), + latestActivities = previewActivityItems.take(3), + balances = previewBalances, + ) + TabBar() + } + } +} + +@Preview(name = "Onboarding Hint", showSystemUi = true) +@Composable +private fun PreviewWithOnboardingHint() { + AppThemeSurface { + Box { + Content( + isRefreshing = false, + homeUiState = HomeUiState( + showWidgets = true, + showWidgetsOnboardingHint = true, + ), + drawerState = rememberDrawerState(initialValue = DrawerValue.Closed), + latestActivities = previewActivityItems.take(3), + balances = previewBalances, + ) + TabBar() + } + } +} + +@Preview(name = "Widgets Page", showSystemUi = true) +@Composable +private fun PreviewWidgetsPage() { + AppThemeSurface { + Box { + Content( + isRefreshing = false, + homeUiState = HomeUiState( + showWidgets = true, + currentPage = 1, + widgetsWithPosition = previewWidgets, + currentBlock = previewBlock, + currentArticle = previewArticle, + currentPrice = previewPrice, + currentWeather = previewWeather, + suggestions = Suggestion.entries.take(4), + ), + drawerState = rememberDrawerState(initialValue = DrawerValue.Closed), + latestActivities = previewActivityItems.take(3), + balances = previewBalances, + ) + TabBar() + } + } +} + +@Preview(name = "Widgets Editing", showSystemUi = true) +@Composable +private fun PreviewWidgetsEditing() { + AppThemeSurface { + Box { + Content( + isRefreshing = false, + homeUiState = HomeUiState( + showWidgets = true, + currentPage = 1, + isEditingWidgets = true, + widgetsWithPosition = previewWidgets, + currentBlock = previewBlock, + currentArticle = previewArticle, + currentPrice = previewPrice, + currentWeather = previewWeather, + ), + drawerState = rememberDrawerState(initialValue = DrawerValue.Closed), + latestActivities = previewActivityItems.take(3), + balances = previewBalances, ) TabBar() } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeUiState.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeUiState.kt index bebb01e6e..b78cb0eff 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeUiState.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeUiState.kt @@ -21,12 +21,12 @@ data class HomeUiState( val banners: List = listOf(), val showWidgets: Boolean = false, val showWidgetTitles: Boolean = false, - val widgetsWithPosition: List = emptyList(), + @Stable val widgetsWithPosition: List = emptyList(), val headlinePreferences: HeadlinePreferences = HeadlinePreferences(), val currentArticle: ArticleModel? = null, val currentFact: String? = null, val factsPreferences: FactsPreferences = FactsPreferences(), - val facts: List = listOf(), + @Stable val facts: List = listOf(), val blocksPreferences: BlocksPreferences = BlocksPreferences(), val currentBlock: BlockModel? = null, val weatherPreferences: WeatherPreferences = WeatherPreferences(), @@ -36,4 +36,6 @@ data class HomeUiState( val isEditingWidgets: Boolean = false, val deleteWidgetAlert: WidgetType? = null, val showEmptyState: Boolean = false, + val currentPage: Int = 0, + val showWidgetsOnboardingHint: Boolean = false, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt index a2e6e563d..fd2f19a54 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt @@ -24,6 +24,7 @@ import to.bitkit.models.toSuggestionOrNull import to.bitkit.models.widget.ArticleModel import to.bitkit.models.widget.toArticleModel import to.bitkit.models.widget.toBlockModel +import to.bitkit.repositories.ActivityRepo import to.bitkit.repositories.TransferRepo import to.bitkit.repositories.WalletRepo import to.bitkit.repositories.WidgetsRepo @@ -31,6 +32,7 @@ import to.bitkit.ui.screens.widgets.blocks.toWeatherModel import javax.inject.Inject import kotlin.time.Duration.Companion.seconds +@Suppress("TooManyFunctions") @HiltViewModel class HomeViewModel @Inject constructor( @ApplicationContext private val context: Context, @@ -38,8 +40,13 @@ class HomeViewModel @Inject constructor( private val widgetsRepo: WidgetsRepo, private val settingsStore: SettingsStore, private val transferRepo: TransferRepo, + private val activityRepo: ActivityRepo, ) : ViewModel() { + companion object { + private const val MAX_SUGGESTIONS = 4 + } + private val _uiState = MutableStateFlow(HomeUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -65,7 +72,11 @@ class HomeViewModel @Inject constructor( suggestions = suggestions, showWidgets = settings.showWidgets, showWidgetTitles = settings.showWidgetTitles, - widgetsWithPosition = widgetsData.widgets, + widgetsWithPosition = if (_uiState.value.isEditingWidgets) { + _uiState.value.widgetsWithPosition + } else { + widgetsData.widgets + }, headlinePreferences = widgetsData.headlinePreferences, factsPreferences = widgetsData.factsPreferences, blocksPreferences = widgetsData.blocksPreferences, @@ -76,6 +87,8 @@ class HomeViewModel @Inject constructor( currentBlock = widgetsData.block?.toBlockModel(), currentWeather = widgetsData.weather?.toWeatherModel(), currentPrice = widgetsData.price, + showWidgetsOnboardingHint = settings.showWidgets && + !settings.widgetsOnboardingHintDismissed, ) }.collect { newState -> _uiState.update { newState } @@ -85,10 +98,19 @@ class HomeViewModel @Inject constructor( viewModelScope.launch { combine( settingsStore.data, - walletRepo.balanceState - ) { settings, balanceState -> + walletRepo.balanceState, + transferRepo.activeTransfers, + activityRepo.activitiesChanged, + ) { settings, balanceState, activeTransfers, _ -> + val hasActivity = activityRepo.getActivities(limit = 1u) + .getOrNull()?.isNotEmpty() == true _uiState.value.copy( - showEmptyState = settings.showEmptyBalanceView && balanceState.totalSats == 0uL + showEmptyState = settings.showEmptyBalanceView && + !hasActivity && + balanceState.totalSats == 0uL && + balanceState.balanceInTransferToSpending == 0uL && + balanceState.balanceInTransferToSavings == 0uL && + activeTransfers.isEmpty() ) }.collect { newState -> _uiState.update { newState } @@ -147,6 +169,16 @@ class HomeViewModel @Inject constructor( _currentFact.value = null } + fun onPageChanged(page: Int) { + _uiState.update { it.copy(currentPage = page) } + } + + fun dismissWidgetsOnboardingHint() { + viewModelScope.launch { + settingsStore.update { it.copy(widgetsOnboardingHintDismissed = true) } + } + } + fun dismissEmptyState() { viewModelScope.launch { settingsStore.update { it.copy(showEmptyBalanceView = false) } @@ -195,7 +227,13 @@ class HomeViewModel @Inject constructor( fun deleteWidget(widgetType: WidgetType) { viewModelScope.launch { widgetsRepo.deleteWidget(widgetType) - dismissAlertDeleteWidget() + _uiState.update { + it.copy( + widgetsWithPosition = it.widgetsWithPosition + .filterNot { widget -> widget.type == widgetType }, + deleteWidgetAlert = null, + ) + } } } @@ -250,32 +288,29 @@ class HomeViewModel @Inject constructor( transferRepo.activeTransfers, ) { balanceState, settings, transfers -> val baseSuggestions = when { - balanceState.totalLightningSats > 0uL -> { // With Lightning + balanceState.totalLightningSats > 0uL -> { // With Lightning (spending) listOfNotNull( - Suggestion.BACK_UP.takeIf { !settings.backupVerified }, - Suggestion.SECURE.takeIf { !settings.isPinEnabled }, - Suggestion.BUY, - Suggestion.SUPPORT, - Suggestion.INVITE, - Suggestion.QUICK_PAY, + Suggestion.QUICK_PAY.takeIf { !settings.isQuickPayEnabled }, Suggestion.NOTIFICATIONS.takeIf { !settings.notificationsGranted }, Suggestion.SHOP, Suggestion.PROFILE, + Suggestion.SUPPORT, + Suggestion.INVITE, + Suggestion.BUY, ) } balanceState.totalOnchainSats > 0uL -> { // Only on chain balance listOfNotNull( Suggestion.BACK_UP.takeIf { !settings.backupVerified }, + Suggestion.SECURE.takeIf { !settings.isPinEnabled }, Suggestion.LIGHTNING.takeIf { transfers.all { it.type != TransferType.TO_SPENDING } }, - Suggestion.SECURE.takeIf { !settings.isPinEnabled }, - Suggestion.BUY, Suggestion.SUPPORT, - Suggestion.INVITE, - Suggestion.SHOP, Suggestion.PROFILE, + Suggestion.INVITE, + Suggestion.BUY, ) } @@ -285,15 +320,15 @@ class HomeViewModel @Inject constructor( Suggestion.LIGHTNING.takeIf { transfers.all { it.type != TransferType.TO_SPENDING } }, + Suggestion.SUPPORT, Suggestion.BACK_UP.takeIf { !settings.backupVerified }, Suggestion.SECURE.takeIf { !settings.isPinEnabled }, - Suggestion.SUPPORT, - Suggestion.INVITE, Suggestion.PROFILE, + Suggestion.INVITE, ) } } val dismissedList = settings.dismissedSuggestions.mapNotNull { it.toSuggestionOrNull() } - baseSuggestions.filterNot { it in dismissedList } + baseSuggestions.filterNot { it in dismissedList }.take(MAX_SUGGESTIONS) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListSimple.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListSimple.kt index cc476b42a..0674198e7 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListSimple.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListSimple.kt @@ -23,28 +23,26 @@ fun ActivityListSimple( items: List?, onAllActivityClick: () -> Unit, onActivityItemClick: (String) -> Unit, - onEmptyActivityRowClick: () -> Unit, ) { + if (items.isNullOrEmpty()) return Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth() ) { - if (items != null && items.isNotEmpty()) { - items.forEachIndexed { index, item -> - ActivityRow(item, onActivityItemClick, testTag = "ActivityShort-$index") + items.forEachIndexed { index, item -> + ActivityRow(item, onActivityItemClick, testTag = "ActivityShort-$index") + if (index < items.lastIndex) { VerticalSpacer(16.dp) } - TertiaryButton( - text = stringResource(R.string.wallet__activity_show_all), - onClick = onAllActivityClick, - modifier = Modifier - .wrapContentWidth() - .padding(top = 8.dp) - .testTag("ActivityShowAll") - ) - } else { - EmptyActivityRow(onClick = onEmptyActivityRowClick) } + TertiaryButton( + text = stringResource(R.string.wallet__activity_show_all), + onClick = onAllActivityClick, + modifier = Modifier + .wrapContentWidth() + .padding(top = 2.dp) + .testTag("ActivityShowAll") + ) } } @@ -56,20 +54,6 @@ private fun Preview() { items = previewActivityItems, onAllActivityClick = {}, onActivityItemClick = {}, - onEmptyActivityRowClick = {}, - ) - } -} - -@Preview -@Composable -private fun PreviewEmpty() { - AppThemeSurface { - ActivityListSimple( - items = emptyList(), - onAllActivityClick = {}, - onActivityItemClick = {}, - onEmptyActivityRowClick = {}, ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt index b711b4141..6d7e4e028 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt @@ -111,7 +111,7 @@ fun ActivityRow( ActivityIcon(activity = item, size = 40.dp, isCpfpChild = isCpfpChild) Spacer(modifier = Modifier.width(16.dp)) Column( - verticalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), modifier = Modifier.weight(1f) ) { TransactionStatusText( diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/AddWidgetsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/AddWidgetsScreen.kt index 300d8a5d3..4c834c242 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/AddWidgetsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/AddWidgetsScreen.kt @@ -1,7 +1,10 @@ package to.bitkit.ui.screens.widgets import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag @@ -10,6 +13,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R import to.bitkit.models.WidgetType +import to.bitkit.ui.components.FillHeight +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon @@ -21,6 +27,8 @@ fun AddWidgetsScreen( onWidgetSelected: (WidgetType) -> Unit, onBackCLick: () -> Unit, fiatSymbol: String, + showWidgets: Boolean = true, + onEnableInSettingsClick: () -> Unit = {}, ) { ScreenColumn { AppTopBar( @@ -30,7 +38,9 @@ fun AddWidgetsScreen( ) Column( - modifier = Modifier.padding(horizontal = 16.dp) + modifier = Modifier + .padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()) ) { SettingsButtonRow( title = stringResource(R.string.widgets__price__name), @@ -38,8 +48,9 @@ fun AddWidgetsScreen( iconRes = R.drawable.widget_chart_line, iconSize = 48.dp, maxLinesSubtitle = 1, + enabled = showWidgets, onClick = { onWidgetSelected(WidgetType.PRICE) }, - modifier = Modifier.testTag("WidgetListItem-price") + modifier = Modifier.testTag("WidgetListItem-price"), ) SettingsButtonRow( title = stringResource(R.string.widgets__news__name), @@ -47,9 +58,9 @@ fun AddWidgetsScreen( iconRes = R.drawable.widget_newspaper, iconSize = 48.dp, maxLinesSubtitle = 1, + enabled = showWidgets, onClick = { onWidgetSelected(WidgetType.NEWS) }, - modifier = Modifier.testTag("WidgetListItem-news") - + modifier = Modifier.testTag("WidgetListItem-news"), ) SettingsButtonRow( title = stringResource(R.string.widgets__blocks__name), @@ -57,8 +68,9 @@ fun AddWidgetsScreen( iconRes = R.drawable.widget_cube, iconSize = 48.dp, maxLinesSubtitle = 1, + enabled = showWidgets, onClick = { onWidgetSelected(WidgetType.BLOCK) }, - modifier = Modifier.testTag("WidgetListItem-blocks") + modifier = Modifier.testTag("WidgetListItem-blocks"), ) SettingsButtonRow( title = stringResource(R.string.widgets__facts__name), @@ -66,8 +78,9 @@ fun AddWidgetsScreen( iconRes = R.drawable.widget_lightbulb, iconSize = 48.dp, maxLinesSubtitle = 1, + enabled = showWidgets, onClick = { onWidgetSelected(WidgetType.FACTS) }, - modifier = Modifier.testTag("WidgetListItem-facts") + modifier = Modifier.testTag("WidgetListItem-facts"), ) SettingsButtonRow( title = stringResource(R.string.widgets__weather__name), @@ -75,8 +88,9 @@ fun AddWidgetsScreen( iconRes = R.drawable.widget_cloud, iconSize = 48.dp, maxLinesSubtitle = 1, + enabled = showWidgets, onClick = { onWidgetSelected(WidgetType.WEATHER) }, - modifier = Modifier.testTag("WidgetListItem-weather") + modifier = Modifier.testTag("WidgetListItem-weather"), ) SettingsButtonRow( title = stringResource(R.string.widgets__calculator__name), @@ -87,8 +101,29 @@ fun AddWidgetsScreen( iconRes = R.drawable.widget_math_operation, iconSize = 48.dp, maxLinesSubtitle = 1, + enabled = showWidgets, onClick = { onWidgetSelected(WidgetType.CALCULATOR) }, - modifier = Modifier.testTag("WidgetListItem-calculator") + modifier = Modifier.testTag("WidgetListItem-calculator"), + ) + SettingsButtonRow( + title = stringResource(R.string.widgets__suggestions__name), + subtitle = stringResource(R.string.widgets__suggestions__description), + iconRes = R.drawable.widget_suggestions, + iconSize = 48.dp, + maxLinesSubtitle = 1, + enabled = showWidgets, + onClick = { onWidgetSelected(WidgetType.SUGGESTIONS) }, + modifier = Modifier.testTag("WidgetListItem-suggestions"), + ) + } + FillHeight() + if (!showWidgets) { + PrimaryButton( + text = stringResource(R.string.widgets__enable_in_settings), + onClick = onEnableInSettingsClick, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) ) } } @@ -101,7 +136,20 @@ private fun Preview() { AddWidgetsScreen( onWidgetSelected = {}, fiatSymbol = "$", - onBackCLick = {} + onBackCLick = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewDisabled() { + AppThemeSurface { + AddWidgetsScreen( + onWidgetSelected = {}, + fiatSymbol = "$", + onBackCLick = {}, + showWidgets = false, ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlockCard.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlockCard.kt index 4f20305d8..e5c823514 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlockCard.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlockCard.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R import to.bitkit.ui.components.BodyMSB -import to.bitkit.ui.components.BodySB +import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.CaptionB import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -86,15 +86,16 @@ fun BlockCard( modifier = Modifier .fillMaxWidth() .testTag("block_card_block_row"), - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - BodySB( + BodySSB( text = "Block", color = Colors.White64, modifier = Modifier.testTag("block_card_block_label") ) - BodySB( + BodyMSB( text = block, color = Colors.White, modifier = Modifier.testTag("block_card_block_text") @@ -107,15 +108,16 @@ fun BlockCard( modifier = Modifier .fillMaxWidth() .testTag("block_card_time_row"), - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - BodySB( + BodySSB( text = "Time", color = Colors.White64, modifier = Modifier.testTag("block_card_time_label") ) - BodySB( + BodyMSB( text = time, color = Colors.White, modifier = Modifier.testTag("block_card_time_text") @@ -128,15 +130,16 @@ fun BlockCard( modifier = Modifier .fillMaxWidth() .testTag("block_card_date_row"), - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - BodySB( + BodySSB( text = "Date", color = Colors.White64, modifier = Modifier.testTag("block_card_date_label") ) - BodySB( + BodyMSB( text = date, color = Colors.White, modifier = Modifier.testTag("block_card_date_text") @@ -149,15 +152,16 @@ fun BlockCard( modifier = Modifier .fillMaxWidth() .testTag("block_card_transactions_row"), - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - BodySB( + BodySSB( text = "Transactions", color = Colors.White64, modifier = Modifier.testTag("block_card_transactions_label") ) - BodySB( + BodyMSB( text = transactions, color = Colors.White, modifier = Modifier.testTag("block_card_transactions_text") @@ -170,15 +174,16 @@ fun BlockCard( modifier = Modifier .fillMaxWidth() .testTag("block_card_size_row"), - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - BodySB( + BodySSB( text = "Size", color = Colors.White64, modifier = Modifier.testTag("block_card_size_label") ) - BodySB( + BodyMSB( text = size, color = Colors.White, modifier = Modifier.testTag("block_card_size_text") @@ -194,7 +199,7 @@ fun BlockCard( .testTag("block_card_source_row"), horizontalArrangement = Arrangement.SpaceBetween ) { - BodySB( + CaptionB( text = stringResource(R.string.widgets__widget__source), color = Colors.White64, modifier = Modifier.testTag("block_card_source_label") diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksPreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksPreviewScreen.kt index f2cd45de3..5666ebe5f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksPreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksPreviewScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.runtime.Composable @@ -109,10 +108,8 @@ fun BlocksPreviewContent( horizontalArrangement = Arrangement.SpaceBetween ) { Headline( - text = AnnotatedString(stringResource(R.string.widgets__blocks__name)), - modifier = Modifier - .width(200.dp) - .testTag("widget_title") + text = AnnotatedString(stringResource(R.string.widgets__blocks__name).replace(" ", "\n")), + modifier = Modifier.testTag("widget_title"), ) Icon( painter = painterResource(R.drawable.widget_cube), diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/WeatherModel.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/WeatherModel.kt index a6ddbd03d..e3ba8293a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/WeatherModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/WeatherModel.kt @@ -1,10 +1,12 @@ package to.bitkit.ui.screens.widgets.blocks import androidx.annotation.StringRes +import androidx.compose.runtime.Immutable import to.bitkit.R import to.bitkit.data.dto.FeeCondition import to.bitkit.data.dto.WeatherDTO +@Immutable data class WeatherModel( @StringRes val title: Int, @StringRes val description: Int, diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorPreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorPreviewScreen.kt index 1a542baa6..ef5f277b3 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorPreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorPreviewScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.runtime.Composable @@ -113,10 +112,10 @@ fun CalculatorPreviewContent( .testTag("header_row") ) { Headline( - text = AnnotatedString(stringResource(R.string.widgets__calculator__name)), - modifier = Modifier - .width(200.dp) - .testTag("widget_title") + text = AnnotatedString( + stringResource(R.string.widgets__calculator__name).replace(" ", "\n"), + ), + modifier = Modifier.testTag("widget_title"), ) Icon( painter = painterResource(R.drawable.widget_math_operation), diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/facts/FactsPreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/facts/FactsPreviewScreen.kt index 10e2d34db..1bc4213df 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/facts/FactsPreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/facts/FactsPreviewScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.runtime.Composable @@ -108,10 +107,8 @@ fun FactsPreviewContent( horizontalArrangement = Arrangement.SpaceBetween ) { Headline( - text = AnnotatedString(stringResource(R.string.widgets__facts__name)), - modifier = Modifier - .width(200.dp) - .testTag("widget_title") + text = AnnotatedString(stringResource(R.string.widgets__facts__name).replace(" ", "\n")), + modifier = Modifier.testTag("widget_title"), ) Icon( painter = painterResource(R.drawable.widget_lightbulb), diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewScreen.kt index 29730b48f..003df5d3a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.runtime.Composable @@ -109,10 +108,8 @@ fun HeadlinesPreviewContent( horizontalArrangement = Arrangement.SpaceBetween ) { Headline( - text = AnnotatedString(stringResource(R.string.widgets__news__name)), - modifier = Modifier - .width(263.dp) - .testTag("widget_title") + text = AnnotatedString(stringResource(R.string.widgets__news__name).replace(" ", "\n")), + modifier = Modifier.testTag("widget_title"), ) Icon( painter = painterResource(R.drawable.widget_newspaper), diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt index 9ca260bd3..4e3d0bf14 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider @@ -128,10 +127,8 @@ fun PricePreviewContent( horizontalArrangement = Arrangement.SpaceBetween ) { Headline( - text = AnnotatedString(stringResource(R.string.widgets__price__name)), - modifier = Modifier - .width(180.dp) - .testTag("widget_title") + text = AnnotatedString(stringResource(R.string.widgets__price__name).replace(" ", "\n")), + modifier = Modifier.testTag("widget_title"), ) Icon( painter = painterResource(R.drawable.widget_chart_line), diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/suggestions/SuggestionsPreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/suggestions/SuggestionsPreviewScreen.kt new file mode 100644 index 000000000..451cc4391 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/suggestions/SuggestionsPreviewScreen.kt @@ -0,0 +1,189 @@ +package to.bitkit.ui.screens.widgets.suggestions + +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.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import to.bitkit.R +import to.bitkit.models.Suggestion +import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.FillHeight +import to.bitkit.ui.components.Headline +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.components.SecondaryButton +import to.bitkit.ui.components.SuggestionCard +import to.bitkit.ui.components.Text13Up +import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.scaffold.AppTopBar +import to.bitkit.ui.scaffold.DrawerNavIcon +import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors + +private val previewSuggestions = listOf(Suggestion.BUY, Suggestion.BACK_UP) + +@Composable +fun SuggestionsPreviewScreen( + suggestionsViewModel: SuggestionsViewModel, + onClose: () -> Unit, + onBack: () -> Unit, +) { + val isSuggestionsWidgetEnabled by suggestionsViewModel.isSuggestionsWidgetEnabled + .collectAsStateWithLifecycle() + + Content( + onBack = onBack, + isSuggestionsWidgetEnabled = isSuggestionsWidgetEnabled, + onClickDelete = { + suggestionsViewModel.removeWidget() + onClose() + }, + onClickSave = { + suggestionsViewModel.addWidget() + onClose() + }, + ) +} + +@Composable +private fun Content( + onBack: () -> Unit, + isSuggestionsWidgetEnabled: Boolean, + onClickDelete: () -> Unit, + onClickSave: () -> Unit, +) { + ScreenColumn { + AppTopBar( + titleText = stringResource(R.string.widgets__widget__nav_title), + onBackClick = onBack, + actions = { DrawerNavIcon() }, + ) + + Column( + modifier = Modifier.padding(horizontal = 16.dp) + ) { + VerticalSpacer(26.dp) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + Headline( + text = AnnotatedString(stringResource(R.string.widgets__suggestions__name).replace(" ", "\n")), + ) + Icon( + painter = painterResource(R.drawable.widget_suggestions), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.size(64.dp) + ) + } + + BodyM( + text = stringResource(R.string.widgets__suggestions__description), + color = Colors.White64, + modifier = Modifier.padding(vertical = 16.dp) + ) + + HorizontalDivider() + + FillHeight() + + Text13Up( + stringResource(R.string.common__preview), + color = Colors.White64, + modifier = Modifier.padding(vertical = 16.dp) + ) + + LazyVerticalGrid( + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + userScrollEnabled = false, + modifier = Modifier.fillMaxWidth() + ) { + items( + items = previewSuggestions, + key = { it.name } + ) { item -> + SuggestionCard( + gradientColor = item.color, + title = stringResource(item.title), + description = stringResource(item.description), + icon = item.icon, + disableGlow = true, + onClick = {}, + ) + } + } + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .padding(vertical = 21.dp) + .fillMaxWidth(), + ) { + if (isSuggestionsWidgetEnabled) { + SecondaryButton( + text = stringResource(R.string.common__delete), + fullWidth = false, + onClick = onClickDelete, + modifier = Modifier.weight(1f), + ) + } + + PrimaryButton( + text = stringResource(R.string.common__save), + fullWidth = false, + onClick = onClickSave, + modifier = Modifier.weight(1f), + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + AppThemeSurface { + Content( + onBack = {}, + isSuggestionsWidgetEnabled = false, + onClickDelete = {}, + onClickSave = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewWithDelete() { + AppThemeSurface { + Content( + onBack = {}, + isSuggestionsWidgetEnabled = true, + onClickDelete = {}, + onClickSave = {}, + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/suggestions/SuggestionsViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/suggestions/SuggestionsViewModel.kt new file mode 100644 index 000000000..afa8b5aa3 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/suggestions/SuggestionsViewModel.kt @@ -0,0 +1,44 @@ +package to.bitkit.ui.screens.widgets.suggestions + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import to.bitkit.models.WidgetType +import to.bitkit.repositories.WidgetsRepo +import javax.inject.Inject + +@HiltViewModel +class SuggestionsViewModel @Inject constructor( + private val widgetsRepo: WidgetsRepo, +) : ViewModel() { + + companion object { + private const val SUBSCRIBE_TIMEOUT = 5000L + } + + val isSuggestionsWidgetEnabled: StateFlow = widgetsRepo.widgetsDataFlow + .map { widgetsData -> + widgetsData.widgets.any { it.type == WidgetType.SUGGESTIONS } + } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(SUBSCRIBE_TIMEOUT), false) + + val showWidgetTitles: StateFlow = widgetsRepo.showWidgetTitles + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(SUBSCRIBE_TIMEOUT), false) + + fun addWidget() { + viewModelScope.launch { + widgetsRepo.addWidget(WidgetType.SUGGESTIONS) + } + } + + fun removeWidget() { + viewModelScope.launch { + widgetsRepo.deleteWidget(WidgetType.SUGGESTIONS) + } + } +} diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/weather/WeatherPreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/weather/WeatherPreviewScreen.kt index 3747138fe..796a8e10f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/weather/WeatherPreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/weather/WeatherPreviewScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider @@ -113,10 +112,8 @@ fun WeatherPreviewContent( horizontalArrangement = Arrangement.SpaceBetween ) { Headline( - text = AnnotatedString(stringResource(R.string.widgets__weather__name)), - modifier = Modifier - .width(200.dp) - .testTag("widget_title") + text = AnnotatedString(stringResource(R.string.widgets__weather__name).replace(" ", "\n")), + modifier = Modifier.testTag("widget_title"), ) Icon( painter = painterResource(R.drawable.widget_cloud), diff --git a/app/src/main/java/to/bitkit/ui/theme/Type.kt b/app/src/main/java/to/bitkit/ui/theme/Type.kt index a2392145b..73cde025f 100644 --- a/app/src/main/java/to/bitkit/ui/theme/Type.kt +++ b/app/src/main/java/to/bitkit/ui/theme/Type.kt @@ -123,7 +123,7 @@ object AppTextStyles { fontWeight = FontWeight.Medium, fontSize = 13.sp, lineHeight = 18.sp, - letterSpacing = 0.4.sp, + letterSpacing = 0.8.sp, fontFamily = InterFontFamily, ) val CaptionB = TextStyle( diff --git a/app/src/main/res/drawable-hdpi/lightbulb.png b/app/src/main/res/drawable-hdpi/lightbulb.png new file mode 100644 index 000000000..e8e402f7c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/lightbulb.png differ diff --git a/app/src/main/res/drawable-mdpi/lightbulb.png b/app/src/main/res/drawable-mdpi/lightbulb.png new file mode 100644 index 000000000..f5e68b21b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/lightbulb.png differ diff --git a/app/src/main/res/drawable-nodpi/lightbulb.png b/app/src/main/res/drawable-nodpi/lightbulb.png deleted file mode 100644 index 6f5838659..000000000 Binary files a/app/src/main/res/drawable-nodpi/lightbulb.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/lightbulb.png b/app/src/main/res/drawable-xhdpi/lightbulb.png new file mode 100644 index 000000000..ef9488e45 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/lightbulb.png differ diff --git a/app/src/main/res/drawable-xxhdpi/lightbulb.png b/app/src/main/res/drawable-xxhdpi/lightbulb.png new file mode 100644 index 000000000..f90abdba2 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/lightbulb.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/lightbulb.png b/app/src/main/res/drawable-xxxhdpi/lightbulb.png new file mode 100644 index 000000000..7dd4f4a8c Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/lightbulb.png differ diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml new file mode 100644 index 000000000..bd9ebb0c4 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_received.xml b/app/src/main/res/drawable/ic_received.xml index ed6a12788..b9a852e1d 100644 --- a/app/src/main/res/drawable/ic_received.xml +++ b/app/src/main/res/drawable/ic_received.xml @@ -1,12 +1,14 @@ - - - + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + + diff --git a/app/src/main/res/drawable/ic_sent.xml b/app/src/main/res/drawable/ic_sent.xml index 45152a55d..02b516e20 100644 --- a/app/src/main/res/drawable/ic_sent.xml +++ b/app/src/main/res/drawable/ic_sent.xml @@ -1,12 +1,14 @@ - - - + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + + diff --git a/app/src/main/res/drawable/ic_timer_alt.xml b/app/src/main/res/drawable/ic_timer_alt.xml index fd04ab152..b740006e1 100644 --- a/app/src/main/res/drawable/ic_timer_alt.xml +++ b/app/src/main/res/drawable/ic_timer_alt.xml @@ -1,5 +1,27 @@ - - - - + + + + + + diff --git a/app/src/main/res/drawable/ic_transfer.xml b/app/src/main/res/drawable/ic_transfer.xml index c2b09d853..2ebdadfdc 100644 --- a/app/src/main/res/drawable/ic_transfer.xml +++ b/app/src/main/res/drawable/ic_transfer.xml @@ -1,22 +1,22 @@ + android:width="16dp" + android:height="16dp" + android:viewportWidth="16" + android:viewportHeight="16"> diff --git a/app/src/main/res/drawable/swipe_instruction.xml b/app/src/main/res/drawable/swipe_instruction.xml new file mode 100644 index 000000000..6ec67c2b2 --- /dev/null +++ b/app/src/main/res/drawable/swipe_instruction.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/app/src/main/res/drawable/widget_suggestions.xml b/app/src/main/res/drawable/widget_suggestions.xml new file mode 100644 index 000000000..f5ce0bbdd --- /dev/null +++ b/app/src/main/res/drawable/widget_suggestions.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 7bad49966..1ce6686ad 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -796,7 +796,7 @@ أُزيل من Mempool مُرسل أُرسل لنفسي - عرض كل النشاط + عرض الكل الحالة ناجح الكل diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml index 16dcbef26..a619d6363 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -796,7 +796,7 @@ Eliminado de Mempool Enviado Enviado a mí mismo - Mostrar toda la actividad + Mostrar todo Estatus Éxito Todas diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index cb5e4ad8c..79d34ae77 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -796,7 +796,7 @@ Eliminat de la Mempool Enviat Enviat a mi mateix - Mostra tota l\'activitat + Mostra-ho tot Estat Exitós Tots diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 7182c5530..48fc95e61 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -796,7 +796,7 @@ Odebráno z Mempoolu Posláno Posláno sobě - Zobrazit veškerou aktivitu + Zobrazit vše Status Úspěch Vše diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 15d9fa57e..949139b26 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -866,7 +866,7 @@ Du wirst empfangen MINIMUM Aktivität - Alle Aktivitäten anzeigen + Alle anzeigen Noch keine Aktivitäten Empfange einige Gelder, um zu starten Gesendet diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index f5f597595..d5cde34c4 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -796,7 +796,7 @@ Αφαιρέθηκε από το Mempool Στάλθηκε Στάλθηκε στον εαυτό μου - Εμφάνιση όλης της δραστηριότητας + Εμφάνιση όλων Κατάσταση Επιτυχής Όλα diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 531548579..243569e5d 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -796,7 +796,7 @@ Eliminado De Mempool Enviado Enviado a mí mismo - Mostrar toda la actividad + Mostrar todo Estatus Éxito Todas diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index e83746275..5d6ad056e 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -861,7 +861,7 @@ Recibirás MÍNIMO Actividad - Mostrar toda la actividad + Mostrar todo Aún no hay actividad Reciba algunos fondos para empezar Enviado diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index e225932ff..6f8a88b7c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -856,7 +856,7 @@ Vous recevrez MINIMUM Activité - Afficher toutes les activités + Tout afficher Pas encore d\'activité Recevoir des fonds pour démarrer Envoyé diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 4820186a7..fe1348e62 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -796,7 +796,7 @@ Rimossa dalla Mempool Inviato Inviato a me stesso - Visualizza tutte le attività + Mostra tutto Stato Successo Tutti diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index d013e28af..2ce90640d 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -796,7 +796,7 @@ Verwijderd uit mempool Verzonden Naar mezelf verzonden - Alle activiteit tonen + Alles tonen Status Succesvol Alles diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 5e471c82d..9a2d808e1 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -856,7 +856,7 @@ Otrzymasz MINIMUM Aktywność - Pokaż całą aktywność + Pokaż wszystko Nie ma jeszcze aktywności Otrzymaj trochę środków aby rozpocząć Wysłane diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 81514d997..304fd5e94 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -796,7 +796,7 @@ Removido da Mempool Enviado Enviado para mim mesmo - Mostrar Todas as Atividades + Mostrar Tudo Status Sucesso Todos diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 3183fda3e..f20ecd545 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -811,7 +811,7 @@ Você receberá MÍNIMO Atividade - Mostrar Todas as Atividades + Mostrar Tudo Nenhuma atividade ainda Receba alguns bitcoins para começar Enviado diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index cac54ff2f..2bfc91051 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -813,7 +813,7 @@ Удалено из мемпула Отправлено Отправлено себе - Показать Все + Показать все Статус Успешно Все diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7115a659a..1dc18fca8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -813,7 +813,7 @@ Removed from Mempool Sent Sent to myself - Show All Activity + Show All Status Successful All @@ -979,6 +979,7 @@ Transfer To Spending Your withdrawal was unsuccessful. Please scan the QR code again or contact support. Add Widget + Enable in Settings Examine various statistics on newly mined Bitcoin Blocks. Block Date @@ -996,8 +997,11 @@ Bitcoin Headlines Enjoy decentralized feeds from your favorite web services, by adding fun and useful widgets to your Bitkit wallet. Hello,\n<accent>Widgets</accent> + Swipe to find\n<accent>your widgets</accent> Check the latest Bitcoin exchange rates for a variety of fiat currencies. Bitcoin Price + Discover everything Bitkit has to offer. + Bitkit Suggestions The next block rate is close to the monthly averages. Average Conditions All clear. Now would be a good time to transact on the blockchain.