diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlockCardTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlockCardTest.kt index 3cc2aaee73..323ddec06b 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlockCardTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlockCardTest.kt @@ -17,236 +17,198 @@ class BlockCardTest { private val testDate = "11/2/2022" private val testTransactions = "2,175" private val testSize = "1,606Kb" + private val testFees = "25 059 357" private val testSource = "mempool.io" @Test fun testBlockCardWithAllElements() { - // Arrange & Act composeTestRule.setContent { AppThemeSurface { BlockCard( - showWidgetTitle = true, showBlock = true, showTime = true, showDate = true, showTransactions = true, showSize = true, + showFees = true, showSource = true, block = testBlock, time = testTime, date = testDate, transactions = testTransactions, size = testSize, - source = testSource + fees = testFees, + source = testSource, ) } } - // Assert all elements exist - composeTestRule.onNodeWithTag("block_card_widget_title_row", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_widget_title_icon", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_widget_title_text", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_block_row", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_time_row", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_date_row", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_transactions_row", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_size_row", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_source_row", useUnmergedTree = true).assertExists() - - // Verify text content - composeTestRule.onNodeWithTag("block_card_block_text", useUnmergedTree = true).assertTextEquals(testBlock) - composeTestRule.onNodeWithTag("block_card_time_text", useUnmergedTree = true).assertTextEquals(testTime) - composeTestRule.onNodeWithTag("block_card_date_text", useUnmergedTree = true).assertTextEquals(testDate) - composeTestRule.onNodeWithTag("block_card_transactions_text", useUnmergedTree = true).assertTextEquals(testTransactions) - composeTestRule.onNodeWithTag("block_card_size_text", useUnmergedTree = true).assertTextEquals(testSize) - composeTestRule.onNodeWithTag("block_card_source_text", useUnmergedTree = true).assertTextEquals(testSource) + composeTestRule.onNodeWithTag("block_row", useUnmergedTree = true).assertExists() + composeTestRule.onNodeWithTag("time_row", useUnmergedTree = true).assertExists() + composeTestRule.onNodeWithTag("date_row", useUnmergedTree = true).assertExists() + composeTestRule.onNodeWithTag("transactions_row", useUnmergedTree = true).assertExists() + composeTestRule.onNodeWithTag("size_row", useUnmergedTree = true).assertExists() + composeTestRule.onNodeWithTag("fees_row", useUnmergedTree = true).assertExists() + composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertExists() + + composeTestRule.onNodeWithTag("block_text", useUnmergedTree = true).assertTextEquals(testBlock) + composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertTextEquals(testTime) + composeTestRule.onNodeWithTag("date_text", useUnmergedTree = true).assertTextEquals(testDate) + composeTestRule.onNodeWithTag("transactions_text", useUnmergedTree = true).assertTextEquals(testTransactions) + composeTestRule.onNodeWithTag("size_text", useUnmergedTree = true).assertTextEquals(testSize) + composeTestRule.onNodeWithTag("fees_text", useUnmergedTree = true).assertTextEquals(testFees) + composeTestRule.onNodeWithTag("source_text", useUnmergedTree = true).assertTextEquals(testSource) } @Test - fun testBlockCardWithoutWidgetTitle() { - // Arrange & Act + fun testBlockCardWithoutSource() { composeTestRule.setContent { AppThemeSurface { BlockCard( - showWidgetTitle = false, showBlock = true, showTime = true, showDate = true, showTransactions = true, showSize = true, - showSource = true, + showFees = true, + showSource = false, block = testBlock, time = testTime, date = testDate, transactions = testTransactions, size = testSize, - source = testSource + fees = testFees, + source = testSource, ) } } - // Assert main elements exist - composeTestRule.onNodeWithTag("block_card_block_row", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_source_row", useUnmergedTree = true).assertExists() - - // Assert widget title elements do not exist - composeTestRule.onNodeWithTag("block_card_widget_title_row", useUnmergedTree = true).assertDoesNotExist() - composeTestRule.onNodeWithTag("block_card_widget_title_icon", useUnmergedTree = true).assertDoesNotExist() - composeTestRule.onNodeWithTag("block_card_widget_title_text", useUnmergedTree = true).assertDoesNotExist() + composeTestRule.onNodeWithTag("block_row", useUnmergedTree = true).assertExists() + composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertDoesNotExist() + composeTestRule.onNodeWithTag("source_text", useUnmergedTree = true).assertDoesNotExist() } @Test - fun testBlockCardWithoutSource() { - // Arrange & Act + fun testBlockCardWithoutFees() { composeTestRule.setContent { AppThemeSurface { BlockCard( - showWidgetTitle = true, showBlock = true, showTime = true, showDate = true, showTransactions = true, showSize = true, - showSource = false, + showFees = false, + showSource = true, block = testBlock, time = testTime, date = testDate, transactions = testTransactions, size = testSize, - source = testSource + fees = testFees, + source = testSource, ) } } - // Assert main elements exist - composeTestRule.onNodeWithTag("block_card_widget_title_row", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_block_row", useUnmergedTree = true).assertExists() - - // Assert source elements do not exist - composeTestRule.onNodeWithTag("block_card_source_row", useUnmergedTree = true).assertDoesNotExist() - composeTestRule.onNodeWithTag("block_card_source_label", useUnmergedTree = true).assertDoesNotExist() - composeTestRule.onNodeWithTag("block_card_source_text", useUnmergedTree = true).assertDoesNotExist() + composeTestRule.onNodeWithTag("block_row", useUnmergedTree = true).assertExists() + composeTestRule.onNodeWithTag("fees_row", useUnmergedTree = true).assertDoesNotExist() + composeTestRule.onNodeWithTag("fees_text", useUnmergedTree = true).assertDoesNotExist() } @Test fun testBlockCardMinimal() { - // Arrange & Act - Only block number shown composeTestRule.setContent { AppThemeSurface { BlockCard( - showWidgetTitle = false, showBlock = true, showTime = false, showDate = false, showTransactions = false, showSize = false, + showFees = false, showSource = false, block = testBlock, time = "", date = "", transactions = "", size = "", - source = "" + fees = "", + source = "", ) } } - // Assert only block row exists - composeTestRule.onNodeWithTag("block_card_block_row", useUnmergedTree = true).assertExists() - - // Assert other elements do not exist - composeTestRule.onNodeWithTag("block_card_widget_title_row", useUnmergedTree = true).assertDoesNotExist() - composeTestRule.onNodeWithTag("block_card_time_row", useUnmergedTree = true).assertDoesNotExist() - composeTestRule.onNodeWithTag("block_card_source_row", useUnmergedTree = true).assertDoesNotExist() + composeTestRule.onNodeWithTag("block_row", useUnmergedTree = true).assertExists() + composeTestRule.onNodeWithTag("time_row", useUnmergedTree = true).assertDoesNotExist() + composeTestRule.onNodeWithTag("date_row", useUnmergedTree = true).assertDoesNotExist() + composeTestRule.onNodeWithTag("transactions_row", useUnmergedTree = true).assertDoesNotExist() + composeTestRule.onNodeWithTag("size_row", useUnmergedTree = true).assertDoesNotExist() + composeTestRule.onNodeWithTag("fees_row", useUnmergedTree = true).assertDoesNotExist() + composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertDoesNotExist() } @Test fun testBlockCardWithEmptyValues() { - // Arrange & Act composeTestRule.setContent { AppThemeSurface { BlockCard( - showWidgetTitle = true, showBlock = true, showTime = true, showDate = true, showTransactions = true, showSize = true, + showFees = true, showSource = true, block = "", time = "", date = "", transactions = "", size = "", - source = "" + fees = "", + source = "", ) } } - // Assert only widget title and labels exist (since values are empty) - composeTestRule.onNodeWithTag("block_card_widget_title_row", useUnmergedTree = true).assertExists() - - // Assert text elements don't exist (since values are empty) - - composeTestRule.onNodeWithTag("block_card_time_label", useUnmergedTree = true).assertDoesNotExist() - composeTestRule.onNodeWithTag("block_card_date_label", useUnmergedTree = true).assertDoesNotExist() - composeTestRule.onNodeWithTag("block_card_transactions_label", useUnmergedTree = true).assertDoesNotExist() - composeTestRule.onNodeWithTag("block_card_size_label", useUnmergedTree = true).assertDoesNotExist() - composeTestRule.onNodeWithTag("block_card_source_label", useUnmergedTree = true).assertDoesNotExist() - - composeTestRule.onNodeWithTag("block_card_block_label", useUnmergedTree = true).assertDoesNotExist() - composeTestRule.onNodeWithTag("block_card_block_text", useUnmergedTree = true).assertDoesNotExist() - composeTestRule.onNodeWithTag("block_card_time_text", useUnmergedTree = true).assertDoesNotExist() - composeTestRule.onNodeWithTag("block_card_date_text", useUnmergedTree = true).assertDoesNotExist() - composeTestRule.onNodeWithTag("block_card_transactions_text", useUnmergedTree = true).assertDoesNotExist() - composeTestRule.onNodeWithTag("block_card_size_text", useUnmergedTree = true).assertDoesNotExist() - composeTestRule.onNodeWithTag("block_card_source_text", useUnmergedTree = true).assertDoesNotExist() + composeTestRule.onNodeWithTag("block_row", useUnmergedTree = true).assertDoesNotExist() + composeTestRule.onNodeWithTag("time_row", useUnmergedTree = true).assertDoesNotExist() + composeTestRule.onNodeWithTag("date_row", useUnmergedTree = true).assertDoesNotExist() + composeTestRule.onNodeWithTag("transactions_row", useUnmergedTree = true).assertDoesNotExist() + composeTestRule.onNodeWithTag("size_row", useUnmergedTree = true).assertDoesNotExist() + composeTestRule.onNodeWithTag("fees_row", useUnmergedTree = true).assertDoesNotExist() + composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertDoesNotExist() } @Test - fun testAllElementsExistInFullConfiguration() { - // Arrange & Act + fun testBlockCardSmallWithAllElements() { composeTestRule.setContent { AppThemeSurface { - BlockCard( - showWidgetTitle = true, + BlockCardSmall( showBlock = true, showTime = true, showDate = true, showTransactions = true, showSize = true, + showFees = true, showSource = true, block = testBlock, time = testTime, date = testDate, transactions = testTransactions, size = testSize, - source = testSource + fees = testFees, + source = testSource, ) } } - // Assert all tagged elements exist - composeTestRule.onNodeWithTag("block_card_widget_title_row", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_widget_title_icon", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_widget_title_text", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_block_row", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_block_label", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_block_text", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_time_row", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_time_label", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_time_text", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_date_row", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_date_label", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_date_text", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_transactions_row", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_transactions_label", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_transactions_text", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_size_row", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_size_label", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_size_text", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_source_row", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_source_label", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("block_card_source_text", useUnmergedTree = true).assertExists() + composeTestRule.onNodeWithTag("block_row", useUnmergedTree = true).assertExists() + composeTestRule.onNodeWithTag("time_row", useUnmergedTree = true).assertExists() + composeTestRule.onNodeWithTag("date_row", useUnmergedTree = true).assertExists() + composeTestRule.onNodeWithTag("transactions_row", useUnmergedTree = true).assertExists() + + composeTestRule.onNodeWithTag("block_text", useUnmergedTree = true).assertTextEquals(testBlock) + composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertTextEquals(testTime) } } diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlocksEditScreenTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlocksEditScreenTest.kt index 906a21ebaf..7f82d68f3f 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlocksEditScreenTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlocksEditScreenTest.kt @@ -22,7 +22,8 @@ class BlocksEditScreenTest { date = "11/2/2022", transactionCount = "2,175", size = "1,606Kb", - source = "mempool.io" + source = "mempool.io", + fees = "25 059 357", ) private val defaultPreferences = BlocksPreferences() @@ -36,6 +37,7 @@ class BlocksEditScreenTest { var dateClicked = false var transactionsClicked = false var sizeClicked = false + var feesClicked = false var sourceClicked = false var resetClicked = false var previewClicked = false @@ -50,6 +52,7 @@ class BlocksEditScreenTest { onClickShowDate = { dateClicked = true }, onClickShowTransactions = { transactionsClicked = true }, onClickShowSize = { sizeClicked = true }, + onClickShowFees = { feesClicked = true }, onClickShowSource = { sourceClicked = true }, onClickReset = { resetClicked = true }, onClickPreview = { previewClicked = true }, @@ -63,13 +66,14 @@ class BlocksEditScreenTest { composeTestRule.onNodeWithTag("blocks_edit_screen").assertExists() composeTestRule.onNodeWithTag("WidgetEditScrollView").assertExists() - // Verify description - composeTestRule.onNodeWithTag("edit_description").assertExists() + // Verify section header + composeTestRule.onNodeWithTag("data_section_header").assertExists() // Verify all setting rows exist - listOf("block", "time", "date", "transactions", "size", "source").forEach { prefix -> + listOf("block", "time", "date", "transactions", "size", "fees", "source").forEach { prefix -> composeTestRule.onNodeWithTag("${prefix}_setting_row").assertExists() composeTestRule.onNodeWithTag("${prefix}_label").assertExists() + composeTestRule.onNodeWithTag("${prefix}_leading_icon", useUnmergedTree = true).assertExists() if (testBlock.getFieldValue(prefix).isNotEmpty()) { composeTestRule.onNodeWithTag("${prefix}_text").assertExists() } @@ -115,6 +119,7 @@ class BlocksEditScreenTest { onClickShowDate = {}, onClickShowTransactions = {}, onClickShowSize = {}, + onClickShowFees = {}, onClickShowSource = {}, onClickReset = { resetClicked = true }, onClickPreview = {}, @@ -146,6 +151,7 @@ class BlocksEditScreenTest { onClickShowDate = {}, onClickShowTransactions = {}, onClickShowSize = {}, + onClickShowFees = {}, onClickShowSource = {}, onClickReset = {}, onClickPreview = {}, @@ -167,6 +173,7 @@ class BlocksEditScreenTest { showDate = false, showTransactions = false, showSize = false, + showFees = false, showSource = false ) @@ -179,6 +186,7 @@ class BlocksEditScreenTest { onClickShowDate = {}, onClickShowTransactions = {}, onClickShowSize = {}, + onClickShowFees = {}, onClickShowSource = {}, onClickReset = {}, onClickPreview = {}, @@ -199,6 +207,7 @@ class BlocksEditScreenTest { var dateClicked = false var transactionsClicked = false var sizeClicked = false + var feesClicked = false var sourceClicked = false var resetClicked = false var previewClicked = false @@ -209,6 +218,7 @@ class BlocksEditScreenTest { showDate = false, showTransactions = false, showSize = true, + showFees = false, showSource = true ) @@ -221,6 +231,7 @@ class BlocksEditScreenTest { onClickShowDate = { dateClicked = true }, onClickShowTransactions = { transactionsClicked = true }, onClickShowSize = { sizeClicked = true }, + onClickShowFees = { feesClicked = true }, onClickShowSource = { sourceClicked = true }, onClickReset = { resetClicked = true }, onClickPreview = { previewClicked = true }, @@ -246,6 +257,9 @@ class BlocksEditScreenTest { composeTestRule.onNodeWithTag("size_toggle_button").performClick() assert(sizeClicked) + composeTestRule.onNodeWithTag("fees_toggle_button").performClick() + assert(feesClicked) + composeTestRule.onNodeWithTag("source_toggle_button").performClick() assert(sourceClicked) @@ -265,7 +279,8 @@ class BlocksEditScreenTest { date = "", transactionCount = "", size = "", - source = "" + source = "", + fees = "", ) composeTestRule.setContent { @@ -277,6 +292,7 @@ class BlocksEditScreenTest { onClickShowDate = {}, onClickShowTransactions = {}, onClickShowSize = {}, + onClickShowFees = {}, onClickShowSource = {}, onClickReset = {}, onClickPreview = {}, @@ -287,7 +303,7 @@ class BlocksEditScreenTest { } // Assert that text elements don't exist when values are empty - listOf("block", "time", "date", "transactions", "size", "source").forEach { prefix -> + listOf("block", "time", "date", "transactions", "size", "fees", "source").forEach { prefix -> composeTestRule.onNodeWithTag("${prefix}_text").assertDoesNotExist() } } @@ -304,6 +320,7 @@ class BlocksEditScreenTest { onClickShowDate = {}, onClickShowTransactions = {}, onClickShowSize = {}, + onClickShowFees = {}, onClickShowSource = {}, onClickReset = {}, onClickPreview = {}, @@ -313,6 +330,7 @@ class BlocksEditScreenTest { showDate = false, showTransactions = false, showSize = true, + showFees = false, showSource = true ), block = testBlock @@ -323,11 +341,12 @@ class BlocksEditScreenTest { // Assert all tagged elements exist composeTestRule.onNodeWithTag("blocks_edit_screen").assertExists() composeTestRule.onNodeWithTag("WidgetEditScrollView").assertExists() - composeTestRule.onNodeWithTag("edit_description").assertExists() + composeTestRule.onNodeWithTag("data_section_header").assertExists() - listOf("block", "time", "date", "transactions", "size", "source").forEach { prefix -> + listOf("block", "time", "date", "transactions", "size", "fees", "source").forEach { prefix -> composeTestRule.onNodeWithTag("${prefix}_setting_row").assertExists() composeTestRule.onNodeWithTag("${prefix}_label").assertExists() + composeTestRule.onNodeWithTag("${prefix}_leading_icon", useUnmergedTree = true).assertExists() composeTestRule.onNodeWithTag("${prefix}_toggle_button").assertExists() composeTestRule.onNodeWithTag("${prefix}_toggle_icon", useUnmergedTree = true).assertExists() composeTestRule.onNodeWithTag("${prefix}_divider").assertExists() @@ -347,6 +366,7 @@ private fun BlockModel.getFieldValue(prefix: String): String { "date" -> date "transactions" -> transactionCount "size" -> size + "fees" -> fees "source" -> source else -> "" } diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlocksPreviewScreenTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlocksPreviewScreenTest.kt index 3a5393d50e..e85b589ab5 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlocksPreviewScreenTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/blocks/BlocksPreviewScreenTest.kt @@ -21,7 +21,8 @@ class BlocksPreviewContentTest { date = "2023-01-01", transactionCount = "2,175", size = "1,606kB", - source = "mempool.space" + source = "mempool.space", + fees = "25 059 357", ) private val defaultPreferences = BlocksPreferences() @@ -41,7 +42,6 @@ class BlocksPreviewContentTest { onClickEdit = { editClicked = true }, onClickDelete = { deleteClicked = true }, onClickSave = { saveClicked = true }, - showWidgetTitles = true, isBlocksWidgetEnabled = true, blocksPreferences = defaultPreferences, block = testBlock @@ -96,7 +96,6 @@ class BlocksPreviewContentTest { onClickEdit = { editClicked = true }, onClickDelete = { deleteClicked = true }, onClickSave = { saveClicked = true }, - showWidgetTitles = false, isBlocksWidgetEnabled = false, blocksPreferences = defaultPreferences, block = testBlock @@ -137,7 +136,6 @@ class BlocksPreviewContentTest { onClickEdit = {}, onClickDelete = {}, onClickSave = {}, - showWidgetTitles = true, isBlocksWidgetEnabled = true, blocksPreferences = customPreferences, block = testBlock @@ -161,7 +159,6 @@ class BlocksPreviewContentTest { onClickEdit = {}, onClickDelete = {}, onClickSave = {}, - showWidgetTitles = true, isBlocksWidgetEnabled = true, blocksPreferences = defaultPreferences, block = testBlock @@ -197,7 +194,6 @@ class BlocksPreviewContentTest { onClickEdit = {}, onClickDelete = {}, onClickSave = {}, - showWidgetTitles = true, isBlocksWidgetEnabled = true, blocksPreferences = defaultPreferences, block = testBlock @@ -228,7 +224,6 @@ class BlocksPreviewContentTest { onClickEdit = {}, onClickDelete = {}, onClickSave = {}, - showWidgetTitles = false, isBlocksWidgetEnabled = false, blocksPreferences = minimalPreferences, block = testBlock @@ -253,7 +248,6 @@ class BlocksPreviewContentTest { onClickEdit = {}, onClickDelete = {}, onClickSave = {}, - showWidgetTitles = true, isBlocksWidgetEnabled = true, blocksPreferences = defaultPreferences, block = testBlock @@ -285,7 +279,6 @@ class BlocksPreviewContentTest { onClickEdit = {}, onClickDelete = {}, onClickSave = {}, - showWidgetTitles = true, isBlocksWidgetEnabled = true, blocksPreferences = customPreferences, block = testBlock @@ -307,7 +300,6 @@ class BlocksPreviewContentTest { onClickEdit = {}, onClickDelete = {}, onClickSave = {}, - showWidgetTitles = true, isBlocksWidgetEnabled = true, blocksPreferences = defaultPreferences, block = null diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1b02cdd87b..74291452ae 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -217,6 +217,19 @@ android:resource="@xml/appwidget_info_headlines" /> + + + + + + + + diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetDataRepository.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetDataRepository.kt index 40b5e9c6ee..be69cdb4ae 100644 --- a/app/src/main/java/to/bitkit/appwidget/AppWidgetDataRepository.kt +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetDataRepository.kt @@ -3,8 +3,10 @@ package to.bitkit.appwidget import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import to.bitkit.data.dto.ArticleDTO +import to.bitkit.data.dto.BlockDTO import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.PriceDTO +import to.bitkit.data.widgets.BlocksService import to.bitkit.data.widgets.NewsService import to.bitkit.data.widgets.PriceService import to.bitkit.di.IoDispatcher @@ -16,6 +18,7 @@ class AppWidgetDataRepository @Inject constructor( @IoDispatcher private val ioDispatcher: CoroutineDispatcher, private val priceService: PriceService, private val newsService: NewsService, + private val blocksService: BlocksService, ) { suspend fun fetchPriceData(period: GraphPeriod = GraphPeriod.ONE_DAY): Result = withContext(ioDispatcher) { @@ -26,4 +29,9 @@ class AppWidgetDataRepository @Inject constructor( withContext(ioDispatcher) { newsService.fetchData() } + + suspend fun fetchBlock(): Result = + withContext(ioDispatcher) { + blocksService.fetchData() + } } diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt index c2692c2cf4..5c089c4c38 100644 --- a/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt @@ -14,6 +14,7 @@ import to.bitkit.appwidget.model.AppWidgetData import to.bitkit.appwidget.model.AppWidgetEntry import to.bitkit.appwidget.model.AppWidgetType import to.bitkit.data.dto.ArticleDTO +import to.bitkit.data.dto.BlockDTO import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.serializers.AppWidgetDataSerializer @@ -32,6 +33,7 @@ interface AppWidgetEntryPoint { } @Singleton +@Suppress("TooManyFunctions") class AppWidgetPreferencesStore @Inject constructor( @ApplicationContext private val context: Context, ) { @@ -90,4 +92,8 @@ class AppWidgetPreferencesStore @Inject constructor( ) } } + + suspend fun cacheBlock(block: BlockDTO) { + store.updateData { it.copy(cachedBlock = block) } + } } diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt index 87166d4050..03ce70786f 100644 --- a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt @@ -16,6 +16,8 @@ import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedInject import to.bitkit.appwidget.model.AppWidgetType +import to.bitkit.appwidget.ui.blocks.BlocksGlanceReceiver +import to.bitkit.appwidget.ui.blocks.BlocksGlanceWidget import to.bitkit.appwidget.ui.headlines.HeadlinesGlanceReceiver import to.bitkit.appwidget.ui.headlines.HeadlinesGlanceWidget import to.bitkit.appwidget.ui.price.PriceGlanceReceiver @@ -65,6 +67,7 @@ class AppWidgetRefreshWorker @AssistedInject constructor( private fun receiverClassFor(type: AppWidgetType): Class = when (type) { AppWidgetType.PRICE -> PriceGlanceReceiver::class.java AppWidgetType.HEADLINES -> HeadlinesGlanceReceiver::class.java + AppWidgetType.BLOCKS -> BlocksGlanceReceiver::class.java } } @@ -96,6 +99,15 @@ class AppWidgetRefreshWorker @AssistedInject constructor( } HeadlinesGlanceWidget().updateAll(appContext) } + + AppWidgetType.BLOCKS -> { + dataRepository.fetchBlock() + .onSuccess { preferencesStore.cacheBlock(it) } + .onFailure { + Logger.warn("Failed to refresh block", it, context = TAG) + } + BlocksGlanceWidget().updateAll(appContext) + } } } diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt index 8cf58ef5d4..8742e48a80 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt @@ -11,6 +11,8 @@ import androidx.glance.appwidget.updateAll import dagger.hilt.android.AndroidEntryPoint import to.bitkit.appwidget.AppWidgetRefreshWorker import to.bitkit.appwidget.model.AppWidgetType +import to.bitkit.appwidget.ui.blocks.BlocksGlanceReceiver +import to.bitkit.appwidget.ui.blocks.BlocksGlanceWidget import to.bitkit.appwidget.ui.headlines.HeadlinesGlanceReceiver import to.bitkit.appwidget.ui.headlines.HeadlinesGlanceWidget import to.bitkit.appwidget.ui.price.PriceGlanceReceiver @@ -55,6 +57,7 @@ class AppWidgetConfigActivity : ComponentActivity() { when (viewModel.uiState.value.type) { AppWidgetType.PRICE -> PriceGlanceWidget().updateAll(this@AppWidgetConfigActivity) AppWidgetType.HEADLINES -> HeadlinesGlanceWidget().updateAll(this@AppWidgetConfigActivity) + AppWidgetType.BLOCKS -> BlocksGlanceWidget().updateAll(this@AppWidgetConfigActivity) } AppWidgetRefreshWorker.enqueue(this@AppWidgetConfigActivity) val result = Intent().putExtra( @@ -80,6 +83,7 @@ class AppWidgetConfigActivity : ComponentActivity() { return when (providerClass) { HeadlinesGlanceReceiver::class.java.name -> AppWidgetType.HEADLINES PriceGlanceReceiver::class.java.name -> AppWidgetType.PRICE + BlocksGlanceReceiver::class.java.name -> AppWidgetType.BLOCKS else -> { Logger.warn( "Encountered unknown provider class '$providerClass' " + diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt index 85661cd1b6..f4da53dc77 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt @@ -1,44 +1,9 @@ package to.bitkit.appwidget.config -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import to.bitkit.R import to.bitkit.appwidget.model.AppWidgetType -import to.bitkit.data.dto.price.GraphPeriod -import to.bitkit.data.dto.price.TradingPair -import to.bitkit.ext.label -import to.bitkit.models.widget.ArticleModel -import to.bitkit.models.widget.HeadlinePreferences -import to.bitkit.models.widget.PricePreferences -import to.bitkit.ui.components.BodySSB -import to.bitkit.ui.components.Caption13Up -import to.bitkit.ui.components.PrimaryButton -import to.bitkit.ui.components.SecondaryButton -import to.bitkit.ui.components.Title -import to.bitkit.ui.components.VerticalSpacer -import to.bitkit.ui.scaffold.AppTopBar -import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.theme.Colors @Composable fun AppWidgetConfigScreen( @@ -49,7 +14,7 @@ fun AppWidgetConfigScreen( val state by viewModel.uiState.collectAsStateWithLifecycle() when (state.type) { - AppWidgetType.PRICE -> Content( + AppWidgetType.PRICE -> PriceConfigContent( state = state, onSelectPair = { viewModel.selectPricePair(it) }, onSelectPeriod = { viewModel.selectPricePeriod(it) }, @@ -66,257 +31,19 @@ fun AppWidgetConfigScreen( onSave = { viewModel.saveAndFinish(onConfirm) }, onCancel = onCancel, ) - } -} - -@Composable -private fun Content( - state: AppWidgetConfigUiState, - onSelectPair: (TradingPair) -> Unit, - onSelectPeriod: (GraphPeriod) -> Unit, - onReset: () -> Unit, - onSave: () -> Unit, - onCancel: () -> Unit, -) { - val prefs = state.pricePreferences - val selectedPair = prefs.enabledPairs.firstOrNull() ?: TradingPair.BTC_USD - - ScreenColumn( - noBackground = true, - modifier = Modifier.background(Colors.Gray7) - ) { - AppTopBar( - titleText = stringResource(R.string.widgets__price__name), - onBackClick = onCancel, - ) - - Column( - modifier = Modifier - .padding(horizontal = 16.dp) - .weight(1f) - .verticalScroll(rememberScrollState()) - ) { - VerticalSpacer(16.dp) - - Caption13Up( - text = stringResource(R.string.appwidget__price__currency), - color = Colors.White64, - modifier = Modifier.padding(bottom = 16.dp) - ) - - for (pair in TradingPair.entries) { - SelectableRow( - label = pair.displayName, - isSelected = pair == selectedPair, - onClick = { onSelectPair(pair) }, - ) - } - - VerticalSpacer(16.dp) - Caption13Up( - text = stringResource(R.string.appwidget__price__timeframe), - color = Colors.White64, - modifier = Modifier.padding(vertical = 16.dp) - ) - - for (period in GraphPeriod.entries) { - SelectableRow( - label = period.label(), - isSelected = period == prefs.period, - onClick = { onSelectPeriod(period) }, - ) - } - } - - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier - .padding(16.dp) - .fillMaxWidth() - ) { - SecondaryButton( - text = stringResource(R.string.common__reset), - enabled = prefs != PricePreferences(), - fullWidth = false, - onClick = onReset, - modifier = Modifier.weight(1f) - ) - PrimaryButton( - text = stringResource(R.string.common__save), - isLoading = state.isSaving, - enabled = !state.isSaving, - fullWidth = false, - onClick = onSave, - modifier = Modifier.weight(1f) - ) - } - } -} - -@Composable -private fun HeadlinesConfigContent( - state: AppWidgetConfigUiState, - onToggleSource: () -> Unit, - onToggleTime: () -> Unit, - onReset: () -> Unit, - onSave: () -> Unit, - onCancel: () -> Unit, -) { - val prefs = state.headlinePreferences - val previewArticle = ArticleModel( - title = stringResource(R.string.widgets__headline__preview_title), - timeAgo = stringResource(R.string.widgets__headline__preview_time), - publisher = stringResource(R.string.widgets__headline__preview_publisher), - link = "", - ) - ScreenColumn( - noBackground = true, - modifier = Modifier.background(Colors.Gray7) - ) { - AppTopBar( - titleText = stringResource(R.string.widgets__news__name), - onBackClick = onCancel, + AppWidgetType.BLOCKS -> BlocksConfigContent( + state = state, + onToggleBlock = { viewModel.toggleBlockShowBlock() }, + onToggleTime = { viewModel.toggleBlockShowTime() }, + onToggleDate = { viewModel.toggleBlockShowDate() }, + onToggleTransactions = { viewModel.toggleBlockShowTransactions() }, + onToggleSize = { viewModel.toggleBlockShowSize() }, + onToggleFees = { viewModel.toggleBlockShowFees() }, + onToggleSource = { viewModel.toggleBlockShowSource() }, + onReset = { viewModel.resetPreferences() }, + onSave = { viewModel.saveAndFinish(onConfirm) }, + onCancel = onCancel, ) - - Column( - modifier = Modifier - .padding(horizontal = 16.dp) - .weight(1f) - .verticalScroll(rememberScrollState()) - ) { - VerticalSpacer(16.dp) - - Caption13Up( - text = stringResource(R.string.widgets__widget__content), - color = Colors.White64, - modifier = Modifier.padding(bottom = 16.dp) - ) - - ToggleRow( - content = { - Title( - text = previewArticle.title, - modifier = Modifier.weight(1f) - ) - }, - isEnabled = true, - onToggle = {}, - toggleEnabled = false, - ) - HorizontalDivider() - - ToggleRow( - content = { - BodySSB( - text = previewArticle.publisher, - color = Colors.Brand, - modifier = Modifier.weight(1f) - ) - }, - isEnabled = prefs.showSource, - onToggle = onToggleSource, - ) - HorizontalDivider() - - ToggleRow( - content = { - BodySSB( - text = previewArticle.timeAgo, - color = Colors.White64, - modifier = Modifier.weight(1f) - ) - }, - isEnabled = prefs.showTime, - onToggle = onToggleTime, - ) - HorizontalDivider() - } - - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier - .padding(16.dp) - .fillMaxWidth() - ) { - SecondaryButton( - text = stringResource(R.string.common__reset), - enabled = prefs != HeadlinePreferences(), - fullWidth = false, - onClick = onReset, - modifier = Modifier.weight(1f) - ) - PrimaryButton( - text = stringResource(R.string.common__save), - isLoading = state.isSaving, - enabled = !state.isSaving, - fullWidth = false, - onClick = onSave, - modifier = Modifier.weight(1f) - ) - } - } -} - -@Composable -private fun ToggleRow( - content: @Composable RowScope.() -> Unit, - isEnabled: Boolean, - onToggle: () -> Unit, - modifier: Modifier = Modifier, - toggleEnabled: Boolean = true, -) { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = modifier - .padding(vertical = 8.dp) - .fillMaxWidth() - ) { - content() - IconButton( - onClick = onToggle, - enabled = toggleEnabled, - ) { - Icon( - painter = painterResource(R.drawable.ic_checkmark), - contentDescription = null, - tint = if (isEnabled) Colors.Brand else Colors.White50, - modifier = Modifier.size(32.dp) - ) - } - } -} - -@Composable -private fun SelectableRow( - label: String, - isSelected: Boolean, - onClick: () -> Unit, -) { - Column { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(vertical = 14.dp) - ) { - BodySSB( - text = label, - color = if (isSelected) Colors.White else Colors.White64, - modifier = Modifier.weight(1f) - ) - if (isSelected) { - Icon( - painter = painterResource(R.drawable.ic_checkmark), - contentDescription = null, - tint = Colors.Brand, - modifier = Modifier.size(32.dp) - ) - } - } - HorizontalDivider() } } diff --git a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt index 887403b103..bd38942e5f 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt @@ -8,21 +8,27 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import to.bitkit.appwidget.AppWidgetDataRepository import to.bitkit.appwidget.AppWidgetPreferencesStore import to.bitkit.appwidget.model.AppWidgetType +import to.bitkit.appwidget.model.HomeBlocksPreferences import to.bitkit.appwidget.model.HomeHeadlinePreferences import to.bitkit.appwidget.model.HomePricePreferences import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.TradingPair +import to.bitkit.models.widget.ArticleModel +import to.bitkit.models.widget.BlocksPreferences import to.bitkit.models.widget.HeadlinePreferences import to.bitkit.models.widget.PricePreferences +import to.bitkit.models.widget.toArticleModel import to.bitkit.utils.Logger import javax.inject.Inject @HiltViewModel +@Suppress("TooManyFunctions") class AppWidgetConfigViewModel @Inject constructor( private val preferencesStore: AppWidgetPreferencesStore, private val dataRepository: AppWidgetDataRepository, @@ -38,6 +44,8 @@ class AppWidgetConfigViewModel @Inject constructor( fun init(appWidgetId: Int, type: AppWidgetType) { viewModelScope.launch { val entry = preferencesStore.getEntry(appWidgetId) + val cachedArticles = preferencesStore.data.first().cachedArticles + val previewArticle = cachedArticles.randomOrNull()?.toArticleModel() ?: DEFAULT_PREVIEW_ARTICLE _uiState.update { it.copy( @@ -45,6 +53,8 @@ class AppWidgetConfigViewModel @Inject constructor( type = type, pricePreferences = entry?.pricePreferences?.toInApp() ?: PricePreferences(), headlinePreferences = entry?.headlinePreferences?.toInApp() ?: HeadlinePreferences(), + blocksPreferences = entry?.blocksPreferences?.toInApp() ?: BlocksPreferences(), + previewArticle = previewArticle, ) } } @@ -78,11 +88,58 @@ class AppWidgetConfigViewModel @Inject constructor( } } + fun toggleBlockShowBlock() { + _uiState.update { + it.copy(blocksPreferences = it.blocksPreferences.copy(showBlock = !it.blocksPreferences.showBlock)) + } + } + + fun toggleBlockShowTime() { + _uiState.update { + it.copy(blocksPreferences = it.blocksPreferences.copy(showTime = !it.blocksPreferences.showTime)) + } + } + + fun toggleBlockShowDate() { + _uiState.update { + it.copy(blocksPreferences = it.blocksPreferences.copy(showDate = !it.blocksPreferences.showDate)) + } + } + + fun toggleBlockShowTransactions() { + _uiState.update { + it.copy( + blocksPreferences = it.blocksPreferences.copy( + showTransactions = !it.blocksPreferences.showTransactions, + ), + ) + } + } + + fun toggleBlockShowSize() { + _uiState.update { + it.copy(blocksPreferences = it.blocksPreferences.copy(showSize = !it.blocksPreferences.showSize)) + } + } + + fun toggleBlockShowFees() { + _uiState.update { + it.copy(blocksPreferences = it.blocksPreferences.copy(showFees = !it.blocksPreferences.showFees)) + } + } + + fun toggleBlockShowSource() { + _uiState.update { + it.copy(blocksPreferences = it.blocksPreferences.copy(showSource = !it.blocksPreferences.showSource)) + } + } + fun resetPreferences() { _uiState.update { when (it.type) { AppWidgetType.PRICE -> it.copy(pricePreferences = PricePreferences()) AppWidgetType.HEADLINES -> it.copy(headlinePreferences = HeadlinePreferences()) + AppWidgetType.BLOCKS -> it.copy(blocksPreferences = BlocksPreferences()) } } } @@ -95,6 +152,7 @@ class AppWidgetConfigViewModel @Inject constructor( when (state.type) { AppWidgetType.PRICE -> savePrice(state) AppWidgetType.HEADLINES -> saveHeadlines(state) + AppWidgetType.BLOCKS -> saveBlocks(state) } onComplete() @@ -126,14 +184,35 @@ class AppWidgetConfigViewModel @Inject constructor( .onSuccess { preferencesStore.cacheArticlesAndRotate(it) } .onFailure { Logger.warn("Failed to fetch initial articles", it, context = TAG) } } + + private suspend fun saveBlocks(state: AppWidgetConfigUiState) { + val appWidgetId = state.appWidgetId + val blocksPreferences = state.blocksPreferences + preferencesStore.registerWidget(appWidgetId, AppWidgetType.BLOCKS) + preferencesStore.updateEntry(appWidgetId) { entry -> + entry.copy(blocksPreferences = blocksPreferences.toHome()) + } + dataRepository.fetchBlock() + .onSuccess { preferencesStore.cacheBlock(it) } + .onFailure { Logger.warn("Failed to fetch initial block", it, context = TAG) } + } } +private val DEFAULT_PREVIEW_ARTICLE = ArticleModel( + title = "How Bitcoin changed El Salvador in more ways", + timeAgo = "21 minutes ago", + publisher = "bitcoinmagazine.com", + link = "", +) + @Stable data class AppWidgetConfigUiState( val appWidgetId: Int = -1, val type: AppWidgetType = AppWidgetType.PRICE, val pricePreferences: PricePreferences = PricePreferences(), val headlinePreferences: HeadlinePreferences = HeadlinePreferences(), + val blocksPreferences: BlocksPreferences = BlocksPreferences(), + val previewArticle: ArticleModel = DEFAULT_PREVIEW_ARTICLE, val isSaving: Boolean = false, ) @@ -156,3 +235,23 @@ private fun HeadlinePreferences.toHome() = HomeHeadlinePreferences( showTime = showTime, showSource = showSource, ) + +private fun HomeBlocksPreferences.toInApp() = BlocksPreferences( + showBlock = showBlock, + showTime = showTime, + showDate = showDate, + showTransactions = showTransactions, + showSize = showSize, + showFees = showFees, + showSource = showSource, +) + +private fun BlocksPreferences.toHome() = HomeBlocksPreferences( + showBlock = showBlock, + showTime = showTime, + showDate = showDate, + showTransactions = showTransactions, + showSize = showSize, + showFees = showFees, + showSource = showSource, +) diff --git a/app/src/main/java/to/bitkit/appwidget/config/BlocksConfigContent.kt b/app/src/main/java/to/bitkit/appwidget/config/BlocksConfigContent.kt new file mode 100644 index 0000000000..70a257e024 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/config/BlocksConfigContent.kt @@ -0,0 +1,208 @@ +package to.bitkit.appwidget.config + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import to.bitkit.R +import to.bitkit.models.widget.BlockModel +import to.bitkit.models.widget.BlocksPreferences +import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.BodySSB +import to.bitkit.ui.components.Caption13Up +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.components.SecondaryButton +import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.scaffold.AppTopBar +import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.theme.Colors + +@Suppress("LongParameterList") +@Composable +internal fun BlocksConfigContent( + state: AppWidgetConfigUiState, + onToggleBlock: () -> Unit, + onToggleTime: () -> Unit, + onToggleDate: () -> Unit, + onToggleTransactions: () -> Unit, + onToggleSize: () -> Unit, + onToggleFees: () -> Unit, + onToggleSource: () -> Unit, + onReset: () -> Unit, + onSave: () -> Unit, + onCancel: () -> Unit, +) { + val prefs = state.blocksPreferences + val previewBlock = remember { + BlockModel( + height = "761,405", + time = "01:31:42 UTC", + date = "11/2/2022", + transactionCount = "2,175", + size = "1,606 Kb", + source = "mempool.io", + fees = "25 059 357", + ) + } + + ScreenColumn( + noBackground = true, + modifier = Modifier.background(Colors.Gray7) + ) { + AppTopBar( + titleText = stringResource(R.string.widgets__blocks__name), + onBackClick = onCancel, + ) + + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .weight(1f) + .verticalScroll(rememberScrollState()) + ) { + VerticalSpacer(16.dp) + + Caption13Up( + text = stringResource(R.string.widgets__widget__data), + color = Colors.White64, + modifier = Modifier.padding(bottom = 16.dp) + ) + + BlockToggleRow( + icon = R.drawable.ic_cube, + label = stringResource(R.string.widgets__blocks__field__block), + value = previewBlock.height, + isEnabled = prefs.showBlock, + onToggle = onToggleBlock, + ) + BlockToggleRow( + icon = R.drawable.ic_clock, + label = stringResource(R.string.widgets__blocks__field__time), + value = previewBlock.time, + isEnabled = prefs.showTime, + onToggle = onToggleTime, + ) + BlockToggleRow( + icon = R.drawable.ic_calendar, + label = stringResource(R.string.widgets__blocks__field__date), + value = previewBlock.date, + isEnabled = prefs.showDate, + onToggle = onToggleDate, + ) + BlockToggleRow( + icon = R.drawable.ic_transfer, + label = stringResource(R.string.widgets__blocks__field__transactions), + value = previewBlock.transactionCount, + isEnabled = prefs.showTransactions, + onToggle = onToggleTransactions, + ) + BlockToggleRow( + icon = R.drawable.ic_file_text, + label = stringResource(R.string.widgets__blocks__field__size), + value = previewBlock.size, + isEnabled = prefs.showSize, + onToggle = onToggleSize, + ) + BlockToggleRow( + icon = R.drawable.ic_coins, + label = stringResource(R.string.widgets__blocks__field__fees), + value = previewBlock.fees, + isEnabled = prefs.showFees, + onToggle = onToggleFees, + ) + BlockToggleRow( + icon = R.drawable.ic_globe, + label = stringResource(R.string.widgets__widget__source), + value = previewBlock.source, + isEnabled = prefs.showSource, + onToggle = onToggleSource, + ) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + ) { + SecondaryButton( + text = stringResource(R.string.common__reset), + enabled = prefs != BlocksPreferences(), + fullWidth = false, + onClick = onReset, + modifier = Modifier.weight(1f) + ) + PrimaryButton( + text = stringResource(R.string.common__save), + isLoading = state.isSaving, + enabled = !state.isSaving && prefs.run { + showBlock || showTime || showDate || showTransactions || showSize || showFees || showSource + }, + fullWidth = false, + onClick = onSave, + modifier = Modifier.weight(1f) + ) + } + } +} + +@Composable +private fun BlockToggleRow( + modifier: Modifier = Modifier, + @DrawableRes icon: Int, + label: String, + value: String, + isEnabled: Boolean, + onToggle: () -> Unit, +) { + Column(modifier = modifier) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + painter = painterResource(icon), + contentDescription = null, + tint = Colors.Brand, + modifier = Modifier.size(20.dp) + ) + BodyM( + text = label, + color = Colors.White80, + modifier = Modifier.weight(1f) + ) + if (value.isNotEmpty()) { + BodySSB( + text = value, + color = Colors.White, + ) + } + IconButton(onClick = onToggle) { + Icon( + painter = painterResource(R.drawable.ic_checkmark), + contentDescription = null, + tint = if (isEnabled) Colors.Brand else Colors.White50, + modifier = Modifier.size(32.dp) + ) + } + } + HorizontalDivider() + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/config/HeadlinesConfigContent.kt b/app/src/main/java/to/bitkit/appwidget/config/HeadlinesConfigContent.kt new file mode 100644 index 0000000000..2138c97703 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/config/HeadlinesConfigContent.kt @@ -0,0 +1,162 @@ +package to.bitkit.appwidget.config + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import to.bitkit.R +import to.bitkit.models.widget.HeadlinePreferences +import to.bitkit.ui.components.BodySSB +import to.bitkit.ui.components.Caption13Up +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.components.SecondaryButton +import to.bitkit.ui.components.Title +import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.scaffold.AppTopBar +import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.theme.Colors + +@Composable +internal fun HeadlinesConfigContent( + state: AppWidgetConfigUiState, + onToggleSource: () -> Unit, + onToggleTime: () -> Unit, + onReset: () -> Unit, + onSave: () -> Unit, + onCancel: () -> Unit, +) { + val prefs = state.headlinePreferences + val previewArticle = state.previewArticle + + ScreenColumn( + noBackground = true, + modifier = Modifier.background(Colors.Gray7) + ) { + AppTopBar( + titleText = stringResource(R.string.widgets__news__name), + onBackClick = onCancel, + ) + + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .weight(1f) + .verticalScroll(rememberScrollState()) + ) { + VerticalSpacer(16.dp) + + Caption13Up( + text = stringResource(R.string.widgets__widget__content), + color = Colors.White64, + modifier = Modifier.padding(bottom = 16.dp) + ) + + ToggleRow( + content = { + Title( + text = previewArticle.title, + modifier = Modifier.weight(1f) + ) + }, + isEnabled = true, + onToggle = {}, + toggleEnabled = false, + ) + HorizontalDivider() + + ToggleRow( + content = { + BodySSB( + text = previewArticle.publisher, + color = Colors.Brand, + modifier = Modifier.weight(1f) + ) + }, + isEnabled = prefs.showSource, + onToggle = onToggleSource, + ) + HorizontalDivider() + + ToggleRow( + content = { + BodySSB( + text = previewArticle.timeAgo, + color = Colors.White64, + modifier = Modifier.weight(1f) + ) + }, + isEnabled = prefs.showTime, + onToggle = onToggleTime, + ) + HorizontalDivider() + } + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + ) { + SecondaryButton( + text = stringResource(R.string.common__reset), + enabled = prefs != HeadlinePreferences(), + fullWidth = false, + onClick = onReset, + modifier = Modifier.weight(1f) + ) + PrimaryButton( + text = stringResource(R.string.common__save), + isLoading = state.isSaving, + enabled = !state.isSaving, + fullWidth = false, + onClick = onSave, + modifier = Modifier.weight(1f) + ) + } + } +} + +@Composable +private fun ToggleRow( + content: @Composable RowScope.() -> Unit, + isEnabled: Boolean, + onToggle: () -> Unit, + modifier: Modifier = Modifier, + toggleEnabled: Boolean = true, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .padding(vertical = 8.dp) + .fillMaxWidth() + ) { + content() + IconButton( + onClick = onToggle, + enabled = toggleEnabled, + ) { + Icon( + painter = painterResource(R.drawable.ic_checkmark), + contentDescription = null, + tint = if (isEnabled) Colors.Brand else Colors.White50, + modifier = Modifier.size(32.dp) + ) + } + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/config/PriceConfigContent.kt b/app/src/main/java/to/bitkit/appwidget/config/PriceConfigContent.kt new file mode 100644 index 0000000000..9fe1fd458c --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/config/PriceConfigContent.kt @@ -0,0 +1,150 @@ +package to.bitkit.appwidget.config + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import to.bitkit.R +import to.bitkit.data.dto.price.GraphPeriod +import to.bitkit.data.dto.price.TradingPair +import to.bitkit.ext.label +import to.bitkit.models.widget.PricePreferences +import to.bitkit.ui.components.BodySSB +import to.bitkit.ui.components.Caption13Up +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.components.SecondaryButton +import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.scaffold.AppTopBar +import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.theme.Colors + +@Composable +internal fun PriceConfigContent( + state: AppWidgetConfigUiState, + onSelectPair: (TradingPair) -> Unit, + onSelectPeriod: (GraphPeriod) -> Unit, + onReset: () -> Unit, + onSave: () -> Unit, + onCancel: () -> Unit, +) { + val prefs = state.pricePreferences + val selectedPair = prefs.enabledPairs.firstOrNull() ?: TradingPair.BTC_USD + + ScreenColumn( + noBackground = true, + modifier = Modifier.background(Colors.Gray7) + ) { + AppTopBar( + titleText = stringResource(R.string.widgets__price__name), + onBackClick = onCancel, + ) + + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .weight(1f) + .verticalScroll(rememberScrollState()) + ) { + VerticalSpacer(16.dp) + + Caption13Up( + text = stringResource(R.string.appwidget__price__currency), + color = Colors.White64, + modifier = Modifier.padding(bottom = 16.dp) + ) + + for (pair in TradingPair.entries) { + SelectableRow( + label = pair.displayName, + isSelected = pair == selectedPair, + onClick = { onSelectPair(pair) }, + ) + } + + VerticalSpacer(16.dp) + Caption13Up( + text = stringResource(R.string.appwidget__price__timeframe), + color = Colors.White64, + modifier = Modifier.padding(vertical = 16.dp) + ) + + for (period in GraphPeriod.entries) { + SelectableRow( + label = period.label(), + isSelected = period == prefs.period, + onClick = { onSelectPeriod(period) }, + ) + } + } + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + ) { + SecondaryButton( + text = stringResource(R.string.common__reset), + enabled = prefs != PricePreferences(), + fullWidth = false, + onClick = onReset, + modifier = Modifier.weight(1f) + ) + PrimaryButton( + text = stringResource(R.string.common__save), + isLoading = state.isSaving, + enabled = !state.isSaving, + fullWidth = false, + onClick = onSave, + modifier = Modifier.weight(1f) + ) + } + } +} + +@Composable +private fun SelectableRow( + label: String, + isSelected: Boolean, + onClick: () -> Unit, +) { + Column { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(vertical = 14.dp) + ) { + BodySSB( + text = label, + color = if (isSelected) Colors.White else Colors.White64, + modifier = Modifier.weight(1f) + ) + if (isSelected) { + Icon( + painter = painterResource(R.drawable.ic_checkmark), + contentDescription = null, + tint = Colors.Brand, + modifier = Modifier.size(32.dp) + ) + } + } + HorizontalDivider() + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt b/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt index 608ad3f976..1328eec788 100644 --- a/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt +++ b/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt @@ -3,6 +3,7 @@ package to.bitkit.appwidget.model import androidx.compose.runtime.Stable import kotlinx.serialization.Serializable import to.bitkit.data.dto.ArticleDTO +import to.bitkit.data.dto.BlockDTO import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.dto.price.TradingPair @@ -10,6 +11,7 @@ import to.bitkit.data.dto.price.TradingPair enum class AppWidgetType { PRICE, HEADLINES, + BLOCKS, } @Stable @@ -19,6 +21,7 @@ data class AppWidgetEntry( val type: AppWidgetType, val pricePreferences: HomePricePreferences = HomePricePreferences(), val headlinePreferences: HomeHeadlinePreferences = HomeHeadlinePreferences(), + val blocksPreferences: HomeBlocksPreferences = HomeBlocksPreferences(), ) @Stable @@ -35,6 +38,18 @@ data class HomeHeadlinePreferences( val showSource: Boolean = true, ) +@Stable +@Serializable +data class HomeBlocksPreferences( + val showBlock: Boolean = true, + val showTime: Boolean = true, + val showDate: Boolean = true, + val showTransactions: Boolean = true, + val showSize: Boolean = false, + val showFees: Boolean = false, + val showSource: Boolean = false, +) + @Stable @Serializable data class AppWidgetData( @@ -42,4 +57,5 @@ data class AppWidgetData( val cachedPrices: Map = emptyMap(), val cachedArticles: List = emptyList(), val articleRotationTick: Int = 0, + val cachedBlock: BlockDTO? = null, ) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceContent.kt new file mode 100644 index 0000000000..7c9299e459 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceContent.kt @@ -0,0 +1,187 @@ +package to.bitkit.appwidget.ui.blocks + +import android.appwidget.AppWidgetManager +import android.content.Context +import android.content.Intent +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.glance.ColorFilter +import androidx.glance.GlanceModifier +import androidx.glance.Image +import androidx.glance.ImageProvider +import androidx.glance.LocalContext +import androidx.glance.LocalSize +import androidx.glance.appwidget.action.actionStartActivity +import androidx.glance.color.ColorProvider +import androidx.glance.layout.Alignment +import androidx.glance.layout.Column +import androidx.glance.layout.Row +import androidx.glance.layout.WidthModifier +import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.fillMaxWidth +import androidx.glance.layout.padding +import androidx.glance.layout.size +import androidx.glance.unit.Dimension +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import to.bitkit.R +import to.bitkit.appwidget.config.AppWidgetConfigActivity +import to.bitkit.appwidget.model.AppWidgetEntry +import to.bitkit.appwidget.model.AppWidgetType +import to.bitkit.appwidget.model.HomeBlocksPreferences +import to.bitkit.appwidget.ui.components.BodyMSB +import to.bitkit.appwidget.ui.components.BodySSB +import to.bitkit.appwidget.ui.components.CaptionB +import to.bitkit.appwidget.ui.components.GlanceLayoutDimens +import to.bitkit.appwidget.ui.components.GlanceWidgetScaffold +import to.bitkit.appwidget.ui.components.HorizontalSpacer +import to.bitkit.appwidget.ui.components.VerticalSpacer +import to.bitkit.appwidget.ui.theme.GlanceColors +import to.bitkit.models.widget.BlockModel +import to.bitkit.ui.theme.Colors + +private const val MAX_SMALL_ROWS = 4 + +private data class BlockRow( + @DrawableRes val icon: Int, + val label: String, + val value: String, +) + +@Suppress("RestrictedApi") +@Composable +fun BlocksGlanceContent( + entry: AppWidgetEntry, + block: BlockModel?, +) { + val context = LocalContext.current + val configIntent = Intent(context, AppWidgetConfigActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, entry.appWidgetId) + putExtra(AppWidgetConfigActivity.EXTRA_WIDGET_TYPE, AppWidgetType.BLOCKS.name) + } + + GlanceWidgetScaffold(onClick = actionStartActivity(configIntent)) { + if (block == null) { + CaptionB(text = context.getString(R.string.appwidget__loading)) + return@GlanceWidgetScaffold + } + + val rows = buildRows(context, entry.blocksPreferences, block) + if (rows.isEmpty()) { + CaptionB(text = context.getString(R.string.appwidget__loading)) + return@GlanceWidgetScaffold + } + + if (LocalSize.current.width >= GlanceLayoutDimens.WIDE_LAYOUT_MIN_WIDTH) { + WideContent(rows = rows) + } else { + CompactContent(rows = rows.take(MAX_SMALL_ROWS).toImmutableList()) + } + } +} + +@Suppress("RestrictedApi") +@Composable +private fun WideContent(rows: ImmutableList) { + Column(modifier = GlanceModifier.fillMaxSize()) { + rows.forEach { row -> + WideRow(row = row, modifier = GlanceModifier.padding(vertical = 6.dp)) + } + } +} + +@Suppress("RestrictedApi") +@Composable +private fun WideRow(row: BlockRow, modifier: GlanceModifier = GlanceModifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.fillMaxWidth() + ) { + Image( + provider = ImageProvider(row.icon), + contentDescription = null, + colorFilter = ColorFilter.tint(ColorProvider(day = Colors.Brand, night = Colors.Brand)), + modifier = GlanceModifier.size(20.dp) + ) + HorizontalSpacer(8.dp) + BodyMSB( + text = row.label, + color = GlanceColors.textSecondary, + modifier = GlanceModifier.then(WidthModifier(Dimension.Expand)) + ) + BodyMSB(text = row.value) + } +} + +@Suppress("RestrictedApi") +@Composable +private fun CompactContent(rows: ImmutableList) { + Column(modifier = GlanceModifier.fillMaxWidth()) { + rows.forEachIndexed { index, row -> + if (index > 0) VerticalSpacer(16.dp) + CompactRow(row = row) + } + } +} + +@Suppress("RestrictedApi") +@Composable +private fun CompactRow(row: BlockRow) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = GlanceModifier.fillMaxWidth() + ) { + Image( + provider = ImageProvider(row.icon), + contentDescription = null, + colorFilter = ColorFilter.tint(ColorProvider(day = Colors.Brand, night = Colors.Brand)), + modifier = GlanceModifier.size(20.dp) + ) + HorizontalSpacer(8.dp) + BodySSB(text = row.value) + } +} + +private fun buildRows( + context: Context, + preferences: HomeBlocksPreferences, + block: BlockModel, +): ImmutableList = listOfNotNull( + BlockRow( + icon = R.drawable.ic_cube, + label = context.getString(R.string.widgets__blocks__field__block), + value = block.height, + ).takeIf { preferences.showBlock && block.height.isNotEmpty() }, + BlockRow( + icon = R.drawable.ic_clock, + label = context.getString(R.string.widgets__blocks__field__time), + value = block.time, + ).takeIf { preferences.showTime && block.time.isNotEmpty() }, + BlockRow( + icon = R.drawable.ic_calendar, + label = context.getString(R.string.widgets__blocks__field__date), + value = block.date, + ).takeIf { preferences.showDate && block.date.isNotEmpty() }, + BlockRow( + icon = R.drawable.ic_transfer, + label = context.getString(R.string.widgets__blocks__field__transactions), + value = block.transactionCount, + ).takeIf { preferences.showTransactions && block.transactionCount.isNotEmpty() }, + BlockRow( + icon = R.drawable.ic_file_text, + label = context.getString(R.string.widgets__blocks__field__size), + value = block.size, + ).takeIf { preferences.showSize && block.size.isNotEmpty() }, + BlockRow( + icon = R.drawable.ic_coins, + label = context.getString(R.string.widgets__blocks__field__fees), + value = block.fees, + ).takeIf { preferences.showFees && block.fees.isNotEmpty() }, + BlockRow( + icon = R.drawable.ic_globe, + label = context.getString(R.string.widgets__widget__source), + value = block.source, + ).takeIf { preferences.showSource && block.source.isNotEmpty() }, +).toImmutableList() diff --git a/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceReceiver.kt b/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceReceiver.kt new file mode 100644 index 0000000000..02299e182d --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceReceiver.kt @@ -0,0 +1,40 @@ +package to.bitkit.appwidget.ui.blocks + +import android.content.Context +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import dagger.hilt.android.EntryPointAccessors +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import to.bitkit.appwidget.AppWidgetEntryPoint +import to.bitkit.appwidget.AppWidgetRefreshWorker + +class BlocksGlanceReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget: GlanceAppWidget = BlocksGlanceWidget() + + override fun onEnabled(context: Context) { + super.onEnabled(context) + AppWidgetRefreshWorker.enqueue(context) + } + + override fun onDeleted(context: Context, appWidgetIds: IntArray) { + super.onDeleted(context, appWidgetIds) + val pendingResult = goAsync() + val store = EntryPointAccessors + .fromApplication(context, AppWidgetEntryPoint::class.java) + .appWidgetPreferencesStore() + CoroutineScope(Dispatchers.IO).launch { + try { + appWidgetIds.forEach { store.unregisterWidget(it) } + } finally { + pendingResult.finish() + } + } + } + + override fun onDisabled(context: Context) { + super.onDisabled(context) + AppWidgetRefreshWorker.cancelIfNoWidgets(context) + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceWidget.kt new file mode 100644 index 0000000000..cc2442e058 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/blocks/BlocksGlanceWidget.kt @@ -0,0 +1,40 @@ +package to.bitkit.appwidget.ui.blocks + +import android.content.Context +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.glance.GlanceId +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.SizeMode +import androidx.glance.appwidget.provideContent +import dagger.hilt.android.EntryPointAccessors +import to.bitkit.appwidget.AppWidgetEntryPoint +import to.bitkit.appwidget.model.AppWidgetData +import to.bitkit.appwidget.model.AppWidgetEntry +import to.bitkit.appwidget.model.AppWidgetType +import to.bitkit.models.widget.toBlockModel + +class BlocksGlanceWidget : GlanceAppWidget() { + + override val sizeMode = SizeMode.Exact + + override suspend fun provideGlance(context: Context, id: GlanceId) { + val store = EntryPointAccessors + .fromApplication(context, AppWidgetEntryPoint::class.java) + .appWidgetPreferencesStore() + val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) + + provideContent { + val data by store.data.collectAsState(initial = AppWidgetData()) + val entry = data.entries.find { it.appWidgetId == appWidgetId } + ?: AppWidgetEntry(appWidgetId = appWidgetId, type = AppWidgetType.BLOCKS) + val block = data.cachedBlock?.toBlockModel() + + BlocksGlanceContent( + entry = entry, + block = block, + ) + } + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceWidgetScaffold.kt b/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceWidgetScaffold.kt index 37aed3364e..e1b8d8d741 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceWidgetScaffold.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/components/GlanceWidgetScaffold.kt @@ -1,12 +1,11 @@ package to.bitkit.appwidget.ui.components -import android.content.Intent import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp import androidx.glance.GlanceModifier import androidx.glance.ImageProvider +import androidx.glance.action.Action import androidx.glance.action.clickable -import androidx.glance.appwidget.action.actionStartActivity import androidx.glance.background import androidx.glance.layout.Column import androidx.glance.layout.fillMaxSize @@ -15,7 +14,7 @@ import to.bitkit.R @Composable fun GlanceWidgetScaffold( - onClick: Intent? = null, + onClick: Action? = null, content: @Composable () -> Unit, ) { val modifier = GlanceModifier @@ -23,7 +22,7 @@ fun GlanceWidgetScaffold( .background(ImageProvider(R.drawable.appwidget_background)) .padding(16.dp) .let { mod -> - if (onClick != null) mod.clickable(actionStartActivity(onClick)) else mod + if (onClick != null) mod.clickable(onClick) else mod } Column(modifier = modifier) { diff --git a/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt index 0ef2e54c24..486353142c 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt @@ -7,6 +7,9 @@ import androidx.compose.ui.unit.dp import androidx.glance.GlanceModifier import androidx.glance.LocalContext import androidx.glance.LocalSize +import androidx.glance.action.actionParametersOf +import androidx.glance.appwidget.action.actionRunCallback +import androidx.glance.appwidget.action.actionStartActivity import androidx.glance.color.ColorProvider import androidx.glance.layout.Alignment import androidx.glance.layout.HeightModifier @@ -28,7 +31,6 @@ import to.bitkit.appwidget.ui.components.VerticalSpacer import to.bitkit.appwidget.ui.theme.GlanceColors import to.bitkit.appwidget.ui.theme.GlanceTextStyles import to.bitkit.models.widget.ArticleModel -import to.bitkit.models.widget.safeBrowserUri import to.bitkit.ui.theme.Colors @Suppress("RestrictedApi") @@ -38,20 +40,20 @@ fun HeadlinesGlanceContent( article: ArticleModel?, ) { val context = LocalContext.current - val articleUri = article?.safeBrowserUri() - val tapIntent = if (articleUri != null) { - Intent(Intent.ACTION_VIEW, articleUri).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } + val tapAction = if (article != null && article.link.isNotEmpty()) { + actionRunCallback( + actionParametersOf(OpenUrlAction.linkKey to article.link) + ) } else { - Intent(context, AppWidgetConfigActivity::class.java).apply { + val configIntent = Intent(context, AppWidgetConfigActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, entry.appWidgetId) putExtra(AppWidgetConfigActivity.EXTRA_WIDGET_TYPE, AppWidgetType.HEADLINES.name) } + actionStartActivity(configIntent) } - GlanceWidgetScaffold(onClick = tapIntent) { + GlanceWidgetScaffold(onClick = tapAction) { if (article == null) { CaptionB(text = context.getString(R.string.appwidget__loading)) return@GlanceWidgetScaffold diff --git a/app/src/main/java/to/bitkit/appwidget/ui/headlines/OpenUrlAction.kt b/app/src/main/java/to/bitkit/appwidget/ui/headlines/OpenUrlAction.kt new file mode 100644 index 0000000000..9d8c23ded7 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/headlines/OpenUrlAction.kt @@ -0,0 +1,31 @@ +package to.bitkit.appwidget.ui.headlines + +import android.content.Context +import android.content.Intent +import androidx.glance.GlanceId +import androidx.glance.action.ActionParameters +import androidx.glance.appwidget.action.ActionCallback +import to.bitkit.models.widget.safeBrowserUri +import to.bitkit.utils.Logger + +private const val TAG = "OpenUrlAction" + +class OpenUrlAction : ActionCallback { + companion object { + val linkKey = ActionParameters.Key("article_link") + } + + override suspend fun onAction( + context: Context, + glanceId: GlanceId, + parameters: ActionParameters, + ) { + val link = parameters[linkKey] ?: return + val uri = safeBrowserUri(link) ?: return + val intent = Intent(Intent.ACTION_VIEW, uri).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + runCatching { context.startActivity(intent) } + .onFailure { Logger.error("Failed to open url '$link'", it, context = TAG) } + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt index f91ebad969..2bd5cec455 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/price/PriceGlanceContent.kt @@ -10,6 +10,7 @@ import androidx.glance.Image import androidx.glance.ImageProvider import androidx.glance.LocalContext import androidx.glance.LocalSize +import androidx.glance.appwidget.action.actionStartActivity import androidx.glance.appwidget.cornerRadius import androidx.glance.color.ColorProvider import androidx.glance.layout.Alignment @@ -51,7 +52,7 @@ fun PriceGlanceContent( putExtra(AppWidgetConfigActivity.EXTRA_WIDGET_TYPE, AppWidgetType.PRICE.name) } - GlanceWidgetScaffold(onClick = configIntent) { + GlanceWidgetScaffold(onClick = actionStartActivity(configIntent)) { if (widget == null) { CaptionB(text = context.getString(R.string.appwidget__loading)) return@GlanceWidgetScaffold diff --git a/app/src/main/java/to/bitkit/data/dto/BlockDTO.kt b/app/src/main/java/to/bitkit/data/dto/BlockDTO.kt index ac297317e5..b273a5139f 100644 --- a/app/src/main/java/to/bitkit/data/dto/BlockDTO.kt +++ b/app/src/main/java/to/bitkit/data/dto/BlockDTO.kt @@ -13,4 +13,5 @@ data class BlockDTO( val difficulty: String, val merkleRoot: String, val source: String, + val fees: String = "", ) diff --git a/app/src/main/java/to/bitkit/data/dto/MempoolBlockInfo.kt b/app/src/main/java/to/bitkit/data/dto/MempoolBlockInfo.kt index 2a236425b9..336dc4671f 100644 --- a/app/src/main/java/to/bitkit/data/dto/MempoolBlockInfo.kt +++ b/app/src/main/java/to/bitkit/data/dto/MempoolBlockInfo.kt @@ -15,5 +15,11 @@ data class MempoolBlockInfo( val size: Long, val weight: Long, val difficulty: Double, - @SerialName("merkle_root") val merkleRoot: String + @SerialName("merkle_root") val merkleRoot: String, + val extras: BlockExtras? = null, +) + +@Serializable +data class BlockExtras( + @SerialName("totalFees") val totalFees: Long? = null, ) diff --git a/app/src/main/java/to/bitkit/data/widgets/BlocksService.kt b/app/src/main/java/to/bitkit/data/widgets/BlocksService.kt index 4195b50aa3..666c29da2c 100644 --- a/app/src/main/java/to/bitkit/data/widgets/BlocksService.kt +++ b/app/src/main/java/to/bitkit/data/widgets/BlocksService.kt @@ -45,7 +45,7 @@ class BlocksService @Inject constructor( } private suspend fun getBlockInfo(hash: String): MempoolBlockInfo { - val response: HttpResponse = client.get("${Env.mempoolBaseUrl}/block/$hash") + val response: HttpResponse = client.get("${Env.mempoolBaseUrl}/v1/block/$hash") return when (response.status.isSuccess()) { true -> { val responseBody = runCatching { response.body() }.getOrElse { @@ -75,6 +75,7 @@ class BlocksService @Inject constructor( // Format other numbers val formattedHeight = numberFormat.format(blockInfo.height) val formattedTransactionCount = numberFormat.format(blockInfo.txCount) + val formattedFees = blockInfo.extras?.totalFees?.let { numberFormat.format(it).replace(',', ' ') }.orEmpty() // Format timestamp to date and time val timestamp = blockInfo.timestamp * 1000L // Convert to milliseconds @@ -88,7 +89,8 @@ class BlocksService @Inject constructor( weight = formattedWeight, difficulty = difficulty, merkleRoot = blockInfo.merkleRoot, - source = Env.mempoolBaseUrl.replace("https://", "").replaceAfter("/", "").replace("/", "") + source = Env.mempoolBaseUrl.replace("https://", "").replaceAfter("/", "").replace("/", ""), + fees = formattedFees, ) } 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 5d260a9d17..7741ec1da1 100644 --- a/app/src/main/java/to/bitkit/models/widget/BlockModel.kt +++ b/app/src/main/java/to/bitkit/models/widget/BlockModel.kt @@ -15,6 +15,7 @@ data class BlockModel( val transactionCount: String, val size: String, val source: String, + val fees: String, ) fun BlockDTO.toBlockModel() = BlockModel( @@ -23,5 +24,6 @@ fun BlockDTO.toBlockModel() = BlockModel( date = this.timestamp.toDateUTC(), transactionCount = this.transactionCount, size = this.size, - source = this.source + source = this.source, + fees = this.fees, ) 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 2ae2d943e3..6d9a911de0 100644 --- a/app/src/main/java/to/bitkit/models/widget/BlocksPreferences.kt +++ b/app/src/main/java/to/bitkit/models/widget/BlocksPreferences.kt @@ -9,7 +9,8 @@ data class BlocksPreferences( val showBlock: Boolean = true, val showTime: Boolean = true, val showDate: Boolean = true, - val showTransactions: Boolean = false, + val showTransactions: Boolean = true, val showSize: Boolean = false, + val showFees: Boolean = false, val showSource: Boolean = false, ) 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 3993ebad7e..32a0930a3e 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 @@ -731,17 +731,18 @@ private fun Widgets( WidgetType.BLOCK -> { homeUiState.currentBlock?.run { BlockCard( - showWidgetTitle = homeUiState.showWidgetTitles, showBlock = homeUiState.blocksPreferences.showBlock, showTime = homeUiState.blocksPreferences.showTime, showDate = homeUiState.blocksPreferences.showDate, showTransactions = homeUiState.blocksPreferences.showTransactions, showSize = homeUiState.blocksPreferences.showSize, + showFees = homeUiState.blocksPreferences.showFees, showSource = homeUiState.blocksPreferences.showSource, time = time, date = date, transactions = transactionCount, size = size, + fees = fees, source = source, block = height, modifier = Modifier @@ -978,6 +979,7 @@ private val previewBlock = BlockModel( transactionCount = "2,175", size = "1,606kB", source = "mempool.io", + fees = "25 059 357", ) private val previewArticle = ArticleModel( 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 e5c8235146..aa94c62b4c 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 @@ -1,50 +1,51 @@ package to.bitkit.ui.screens.widgets.blocks +import androidx.annotation.DrawableRes import androidx.compose.foundation.background 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.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R +import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.BodySSB -import to.bitkit.ui.components.CaptionB +import to.bitkit.ui.screens.widgets.components.WidgetCardDimens import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +@Suppress("CyclomaticComplexMethod") @Composable fun BlockCard( modifier: Modifier = Modifier, - showWidgetTitle: Boolean, showBlock: Boolean, showTime: Boolean, showDate: Boolean, showTransactions: Boolean, showSize: Boolean, + showFees: Boolean, showSource: Boolean, block: String, time: String, date: String, transactions: String, size: String, + fees: String, source: String, ) { Box( @@ -53,224 +54,286 @@ fun BlockCard( .background(Colors.White10) ) { Column( + verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier .fillMaxWidth() - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + .padding(16.dp) ) { - if (showWidgetTitle) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(bottom = 8.dp) - .testTag("block_card_widget_title_row") - ) { - Icon( - painter = painterResource(R.drawable.widget_cube), - contentDescription = null, - modifier = Modifier - .size(32.dp) - .testTag("block_card_widget_title_icon"), - tint = Color.Unspecified - ) - Spacer(modifier = Modifier.width(16.dp)) - BodyMSB( - text = stringResource(R.string.widgets__blocks__name), - modifier = Modifier.testTag("block_card_widget_title_text") - ) - } - } - if (showBlock && block.isNotEmpty()) { - Row( - modifier = Modifier - .fillMaxWidth() - .testTag("block_card_block_row"), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - BodySSB( - text = "Block", - color = Colors.White64, - modifier = Modifier.testTag("block_card_block_label") - ) - - BodyMSB( - text = block, - color = Colors.White, - modifier = Modifier.testTag("block_card_block_text") - ) - } + WidgetDataRow( + icon = R.drawable.ic_cube, + label = stringResource(R.string.widgets__blocks__field__block), + value = block, + testTagPrefix = "block", + ) } - if (showTime && time.isNotEmpty()) { - Row( - modifier = Modifier - .fillMaxWidth() - .testTag("block_card_time_row"), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - BodySSB( - text = "Time", - color = Colors.White64, - modifier = Modifier.testTag("block_card_time_label") - ) - - BodyMSB( - text = time, - color = Colors.White, - modifier = Modifier.testTag("block_card_time_text") - ) - } + WidgetDataRow( + icon = R.drawable.ic_clock, + label = stringResource(R.string.widgets__blocks__field__time), + value = time, + testTagPrefix = "time", + ) } - if (showDate && date.isNotEmpty()) { - Row( - modifier = Modifier - .fillMaxWidth() - .testTag("block_card_date_row"), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - BodySSB( - text = "Date", - color = Colors.White64, - modifier = Modifier.testTag("block_card_date_label") - ) - - BodyMSB( - text = date, - color = Colors.White, - modifier = Modifier.testTag("block_card_date_text") - ) - } + WidgetDataRow( + icon = R.drawable.ic_calendar, + label = stringResource(R.string.widgets__blocks__field__date), + value = date, + testTagPrefix = "date", + ) } - if (showTransactions && transactions.isNotEmpty()) { - Row( - modifier = Modifier - .fillMaxWidth() - .testTag("block_card_transactions_row"), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - BodySSB( - text = "Transactions", - color = Colors.White64, - modifier = Modifier.testTag("block_card_transactions_label") - ) - - BodyMSB( - text = transactions, - color = Colors.White, - modifier = Modifier.testTag("block_card_transactions_text") - ) - } + WidgetDataRow( + icon = R.drawable.ic_transfer, + label = stringResource(R.string.widgets__blocks__field__transactions), + value = transactions, + testTagPrefix = "transactions", + ) } - if (showSize && size.isNotEmpty()) { - Row( - modifier = Modifier - .fillMaxWidth() - .testTag("block_card_size_row"), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - BodySSB( - text = "Size", - color = Colors.White64, - modifier = Modifier.testTag("block_card_size_label") - ) - - BodyMSB( - text = size, - color = Colors.White, - modifier = Modifier.testTag("block_card_size_text") - ) - } + WidgetDataRow( + icon = R.drawable.ic_file_text, + label = stringResource(R.string.widgets__blocks__field__size), + value = size, + testTagPrefix = "size", + ) + } + if (showFees && fees.isNotEmpty()) { + WidgetDataRow( + icon = R.drawable.ic_coins, + label = stringResource(R.string.widgets__blocks__field__fees), + value = fees, + testTagPrefix = "fees", + ) } - if (showSource && source.isNotEmpty()) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp) - .testTag("block_card_source_row"), - horizontalArrangement = Arrangement.SpaceBetween - ) { - CaptionB( - text = stringResource(R.string.widgets__widget__source), - color = Colors.White64, - modifier = Modifier.testTag("block_card_source_label") - ) + WidgetDataRow( + icon = R.drawable.ic_globe, + label = stringResource(R.string.widgets__widget__source), + value = source, + testTagPrefix = "source", + ) + } + } + } +} - CaptionB( - text = source, - color = Colors.White64, - modifier = Modifier.testTag("block_card_source_text") - ) - } +@Suppress("CyclomaticComplexMethod") +@Composable +fun BlockCardSmall( + modifier: Modifier = Modifier, + showBlock: Boolean, + showTime: Boolean, + showDate: Boolean, + showTransactions: Boolean, + showSize: Boolean, + showFees: Boolean, + showSource: Boolean, + block: String, + time: String, + date: String, + transactions: String, + size: String, + fees: String, + source: String, +) { + val rows = listOfNotNull( + SmallRowData(R.drawable.ic_cube, block, "block").takeIf { showBlock && block.isNotEmpty() }, + SmallRowData(R.drawable.ic_clock, time, "time").takeIf { showTime && time.isNotEmpty() }, + SmallRowData(R.drawable.ic_calendar, date, "date").takeIf { showDate && date.isNotEmpty() }, + SmallRowData(R.drawable.ic_transfer, transactions, "transactions") + .takeIf { showTransactions && transactions.isNotEmpty() }, + SmallRowData(R.drawable.ic_file_text, size, "size").takeIf { showSize && size.isNotEmpty() }, + SmallRowData(R.drawable.ic_coins, fees, "fees").takeIf { showFees && fees.isNotEmpty() }, + SmallRowData(R.drawable.ic_globe, source, "source").takeIf { showSource && source.isNotEmpty() }, + ).take(MAX_SMALL_ROWS) + + Box( + modifier = modifier + .size(WidgetCardDimens.COMPACT_CARD_SIZE) + .clip(shape = MaterialTheme.shapes.medium) + .background(Colors.White10) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + rows.forEach { row -> + SmallDataRow( + icon = row.icon, + value = row.value, + testTagPrefix = row.testTagPrefix, + ) } } } } +private const val MAX_SMALL_ROWS = 4 + +private data class SmallRowData( + @DrawableRes val icon: Int, + val value: String, + val testTagPrefix: String, +) + +@Composable +private fun WidgetDataRow( + modifier: Modifier = Modifier, + @DrawableRes icon: Int, + label: String, + value: String, + testTagPrefix: String, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .testTag("${testTagPrefix}_row") + ) { + Icon( + painter = painterResource(icon), + contentDescription = null, + tint = Colors.Brand, + modifier = Modifier + .size(20.dp) + .testTag("${testTagPrefix}_icon") + ) + BodyM( + text = label, + color = Colors.White80, + modifier = Modifier + .weight(1f) + .testTag("${testTagPrefix}_label") + ) + BodyMSB( + text = value, + color = Colors.White, + modifier = Modifier.testTag("${testTagPrefix}_text") + ) + } +} + +@Composable +private fun SmallDataRow( + modifier: Modifier = Modifier, + @DrawableRes icon: Int, + value: String, + testTagPrefix: String, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .testTag("${testTagPrefix}_row") + ) { + Icon( + painter = painterResource(icon), + contentDescription = null, + tint = Colors.Brand, + modifier = Modifier + .size(20.dp) + .testTag("${testTagPrefix}_icon") + ) + BodySSB( + text = value, + color = Colors.White, + modifier = Modifier.testTag("${testTagPrefix}_text") + ) + } +} + @Preview(showBackground = true) @Composable -private fun FullBlockCardPreview() { +private fun PreviewLargeAll() { AppThemeSurface { Column( + verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier .fillMaxSize() - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + .padding(16.dp) ) { BlockCard( - showWidgetTitle = true, showBlock = true, showTime = true, showDate = true, showTransactions = true, showSize = true, + showFees = true, showSource = true, block = "761,405", time = "01:31:42 UTC", date = "11/2/2022", transactions = "2,175", size = "1,606Kb", + fees = "25 059 357", source = "mempool.io", + modifier = Modifier.fillMaxWidth() ) + } + } +} +@Preview(showBackground = true) +@Composable +private fun PreviewLargeDefault() { + AppThemeSurface { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { BlockCard( - showWidgetTitle = false, showBlock = true, showTime = true, showDate = true, showTransactions = true, - showSize = true, + showSize = false, + showFees = false, showSource = false, block = "761,405", time = "01:31:42 UTC", date = "11/2/2022", transactions = "2,175", - size = "1,606Kb", - source = "mempool.io", // Source text is still provided but won't be shown + size = "", + fees = "", + source = "", + modifier = Modifier.fillMaxWidth() ) + } + } +} - BlockCard( - showWidgetTitle = true, +@Preview(showBackground = true) +@Composable +private fun PreviewSmall() { + AppThemeSurface { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + BlockCardSmall( showBlock = true, - showTime = false, - showDate = false, - showTransactions = false, + showTime = true, + showDate = true, + showTransactions = true, showSize = false, + showFees = false, showSource = false, block = "761,405", - time = "", - date = "", - transactions = "", + time = "01:31:42 UTC", + date = "11/2/2022", + transactions = "2,175", size = "", + fees = "", source = "", ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksEditScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksEditScreen.kt index 1ebb76e95c..cbf57f1d88 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksEditScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksEditScreen.kt @@ -1,15 +1,13 @@ package to.bitkit.ui.screens.widgets.blocks +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -26,12 +24,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.models.widget.BlockModel import to.bitkit.models.widget.BlocksPreferences -import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodySSB +import to.bitkit.ui.components.Caption13Up +import to.bitkit.ui.components.FillHeight import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton +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 @@ -51,10 +50,11 @@ fun BlocksEditScreen( date = "", transactionCount = "", size = "", - source = "" + source = "", + fees = "", ) - BlocksEditContent( + Content( onBack = onBack, blocksPreferences = customPreference, block = currentBlock ?: blockPlaceholder, @@ -63,6 +63,7 @@ fun BlocksEditScreen( onClickShowDate = { blocksViewModel.toggleShowDate() }, onClickShowTransactions = { blocksViewModel.toggleShowTransactions() }, onClickShowSize = { blocksViewModel.toggleShowSize() }, + onClickShowFees = { blocksViewModel.toggleShowFees() }, onClickShowSource = { blocksViewModel.toggleShowSource() }, onClickReset = { blocksViewModel.resetCustomPreferences() }, onClickPreview = navigatePreview, @@ -70,13 +71,14 @@ fun BlocksEditScreen( } @Composable -fun BlocksEditContent( +private fun Content( onBack: () -> Unit, onClickShowBlock: () -> Unit, onClickShowTime: () -> Unit, onClickShowDate: () -> Unit, onClickShowTransactions: () -> Unit, onClickShowSize: () -> Unit, + onClickShowFees: () -> Unit, onClickShowSource: () -> Unit, onClickReset: () -> Unit, onClickPreview: () -> Unit, @@ -84,123 +86,132 @@ fun BlocksEditContent( block: BlockModel, ) { ScreenColumn( - modifier = Modifier.testTag("blocks_edit_screen") + noBackground = true, + modifier = Modifier + .background(Colors.Gray7) + .testTag("blocks_edit_screen") ) { AppTopBar( - titleText = stringResource(R.string.widgets__widget__edit), + titleText = stringResource(R.string.widgets__blocks__name), onBackClick = onBack, - actions = { DrawerNavIcon() }, ) Column( modifier = Modifier .padding(horizontal = 16.dp) - .weight(1f) - .verticalScroll(rememberScrollState()) .testTag("WidgetEditScrollView") ) { - Spacer(modifier = Modifier.height(26.dp)) + VerticalSpacer(16.dp) - BodyM( - text = stringResource(R.string.widgets__widget__edit_description).replace( - "{name}", - stringResource(R.string.widgets__blocks__name) - ), + Caption13Up( + text = stringResource(R.string.widgets__widget__data), color = Colors.White64, - modifier = Modifier.testTag("edit_description") + modifier = Modifier + .padding(bottom = 16.dp) + .testTag("data_section_header") ) - Spacer(modifier = Modifier.height(32.dp)) - - // Block number toggle BlockEditOptionRow( + leadingIcon = R.drawable.ic_cube, label = stringResource(R.string.widgets__blocks__field__block), value = block.height, isEnabled = blocksPreferences.showBlock, onClick = onClickShowBlock, - testTagPrefix = "block" + testTagPrefix = "block", ) - // Time toggle BlockEditOptionRow( + leadingIcon = R.drawable.ic_clock, label = stringResource(R.string.widgets__blocks__field__time), value = block.time, isEnabled = blocksPreferences.showTime, onClick = onClickShowTime, - testTagPrefix = "time" + testTagPrefix = "time", ) - // Date toggle BlockEditOptionRow( + leadingIcon = R.drawable.ic_calendar, label = stringResource(R.string.widgets__blocks__field__date), value = block.date, isEnabled = blocksPreferences.showDate, onClick = onClickShowDate, - testTagPrefix = "date" + testTagPrefix = "date", ) - // Transactions toggle BlockEditOptionRow( + leadingIcon = R.drawable.ic_transfer, label = stringResource(R.string.widgets__blocks__field__transactions), value = block.transactionCount, isEnabled = blocksPreferences.showTransactions, onClick = onClickShowTransactions, - testTagPrefix = "transactions" + testTagPrefix = "transactions", ) - // Size toggle BlockEditOptionRow( + leadingIcon = R.drawable.ic_file_text, label = stringResource(R.string.widgets__blocks__field__size), value = block.size, isEnabled = blocksPreferences.showSize, onClick = onClickShowSize, - testTagPrefix = "size" + testTagPrefix = "size", ) - // Source toggle BlockEditOptionRow( + leadingIcon = R.drawable.ic_coins, + label = stringResource(R.string.widgets__blocks__field__fees), + value = block.fees, + isEnabled = blocksPreferences.showFees, + onClick = onClickShowFees, + testTagPrefix = "fees", + ) + + BlockEditOptionRow( + leadingIcon = R.drawable.ic_globe, label = stringResource(R.string.widgets__widget__source), value = block.source, isEnabled = blocksPreferences.showSource, onClick = onClickShowSource, - testTagPrefix = "source" + testTagPrefix = "source", ) - } - Row( - modifier = Modifier - .padding(vertical = 21.dp, horizontal = 16.dp) - .fillMaxWidth() - .testTag("buttons_row"), - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - SecondaryButton( - text = stringResource(R.string.common__reset), - modifier = Modifier - .weight(1f) - .testTag("WidgetEditReset"), - enabled = blocksPreferences != BlocksPreferences(), - fullWidth = false, - onClick = onClickReset - ) + FillHeight() - PrimaryButton( - text = stringResource(R.string.common__preview), - enabled = blocksPreferences.run { - showBlock || showTime || showDate || showTransactions || showSize || showSource - }, + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier - .weight(1f) - .testTag("WidgetEditPreview"), - fullWidth = false, - onClick = onClickPreview - ) + .padding(vertical = 21.dp) + .fillMaxWidth() + .testTag("buttons_row") + ) { + SecondaryButton( + text = stringResource(R.string.common__reset), + enabled = blocksPreferences != BlocksPreferences(), + fullWidth = false, + onClick = onClickReset, + modifier = Modifier + .weight(1f) + .testTag("WidgetEditReset") + ) + + PrimaryButton( + text = stringResource(R.string.common__preview), + enabled = blocksPreferences.run { + showBlock || showTime || showDate || showTransactions || showSize || showFees || showSource + }, + fullWidth = false, + onClick = onClickPreview, + modifier = Modifier + .weight(1f) + .testTag("WidgetEditPreview") + ) + } } } } @Composable private fun BlockEditOptionRow( + @DrawableRes leadingIcon: Int, label: String, value: String, isEnabled: Boolean, @@ -212,13 +223,21 @@ private fun BlockEditOptionRow( horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .padding(vertical = 21.dp) .fillMaxWidth() .testTag("${testTagPrefix}_setting_row") ) { + Icon( + painter = painterResource(leadingIcon), + contentDescription = null, + tint = Colors.Brand, + modifier = Modifier + .size(20.dp) + .testTag("${testTagPrefix}_leading_icon") + ) + BodySSB( text = label, - color = Colors.White64, + color = Colors.White80, modifier = Modifier .weight(1f) .testTag("${testTagPrefix}_label") @@ -242,7 +261,7 @@ private fun BlockEditOptionRow( tint = if (isEnabled) Colors.Brand else Colors.White50, modifier = Modifier .size(32.dp) - .testTag("${testTagPrefix}_toggle_icon"), + .testTag("${testTagPrefix}_toggle_icon") ) } } @@ -257,13 +276,14 @@ private fun BlockEditOptionRow( @Composable private fun Preview() { AppThemeSurface { - BlocksEditContent( + Content( onBack = {}, onClickShowBlock = {}, onClickShowTime = {}, onClickShowDate = {}, onClickShowTransactions = {}, onClickShowSize = {}, + onClickShowFees = {}, onClickShowSource = {}, onClickReset = {}, onClickPreview = {}, @@ -274,7 +294,8 @@ private fun Preview() { date = "01/2/2022", transactionCount = "2,175", size = "1,606kB", - source = "mempool.io" + source = "mempool.io", + fees = "25 059 357", ), ) } @@ -284,13 +305,14 @@ private fun Preview() { @Composable private fun PreviewWithSomeOptionsEnabled() { AppThemeSurface { - BlocksEditContent( + Content( onBack = {}, onClickShowBlock = {}, onClickShowTime = {}, onClickShowDate = {}, onClickShowTransactions = {}, onClickShowSize = {}, + onClickShowFees = {}, onClickShowSource = {}, onClickReset = {}, onClickPreview = {}, @@ -300,7 +322,8 @@ private fun PreviewWithSomeOptionsEnabled() { showDate = false, showTransactions = true, showSize = false, - showSource = true + showFees = false, + showSource = true, ), block = BlockModel( height = "", @@ -308,7 +331,8 @@ private fun PreviewWithSomeOptionsEnabled() { date = "", transactionCount = "", size = "", - source = "" + source = "", + fees = "", ), ) } @@ -318,13 +342,14 @@ private fun PreviewWithSomeOptionsEnabled() { @Composable private fun PreviewWithAllDisabled() { AppThemeSurface { - BlocksEditContent( + Content( onBack = {}, onClickShowBlock = {}, onClickShowTime = {}, onClickShowDate = {}, onClickShowTransactions = {}, onClickShowSize = {}, + onClickShowFees = {}, onClickShowSource = {}, onClickReset = {}, onClickPreview = {}, @@ -334,7 +359,8 @@ private fun PreviewWithAllDisabled() { showDate = false, showTransactions = false, showSize = false, - showSource = false + showFees = false, + showSource = false, ), block = BlockModel( height = "", @@ -342,7 +368,8 @@ private fun PreviewWithAllDisabled() { date = "", transactionCount = "", size = "", - source = "" + source = "", + fees = "", ), ) } 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 7fb89a6aca..8f6a30b396 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 @@ -1,42 +1,33 @@ package to.bitkit.ui.screens.widgets.blocks +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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.platform.testTag -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.ext.spaceToNewline import to.bitkit.models.widget.BlockModel import to.bitkit.models.widget.BlocksPreferences import to.bitkit.ui.components.BodyM -import to.bitkit.ui.components.Headline import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton -import to.bitkit.ui.components.Text13Up +import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.components.settings.SettingsButtonValue import to.bitkit.ui.scaffold.AppTopBar -import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.screens.widgets.components.WidgetSizeCarousel import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -47,7 +38,6 @@ fun BlocksPreviewScreen( onBack: () -> Unit, navigateEditWidget: () -> Unit, ) { - val showWidgetTitles by blocksViewModel.showWidgetTitles.collectAsStateWithLifecycle() val customBlocksPreferences by blocksViewModel.customPreferences.collectAsStateWithLifecycle() val currentBlock by blocksViewModel.currentBlock.collectAsStateWithLifecycle() val isBlocksWidgetEnabled by blocksViewModel.isBlocksWidgetEnabled.collectAsStateWithLifecycle() @@ -56,11 +46,10 @@ fun BlocksPreviewScreen( blocksViewModel.refreshOnDisplay() } - BlocksPreviewContent( + Content( onBack = onBack, isBlocksWidgetEnabled = isBlocksWidgetEnabled, blocksPreferences = customBlocksPreferences, - showWidgetTitles = showWidgetTitles, block = currentBlock, onClickEdit = navigateEditWidget, onClickDelete = { @@ -75,136 +64,138 @@ fun BlocksPreviewScreen( } @Composable -fun BlocksPreviewContent( +private fun Content( onBack: () -> Unit, onClickEdit: () -> Unit, onClickDelete: () -> Unit, onClickSave: () -> Unit, - showWidgetTitles: Boolean, isBlocksWidgetEnabled: Boolean, blocksPreferences: BlocksPreferences, block: BlockModel?, ) { ScreenColumn( - modifier = Modifier.testTag("blocks_preview_screen") + noBackground = true, + modifier = Modifier + .background(Colors.Gray7) + .testTag("blocks_preview_screen") ) { AppTopBar( - titleText = stringResource(R.string.widgets__widget__nav_title), + titleText = stringResource(R.string.widgets__blocks__name), onBackClick = onBack, - actions = { DrawerNavIcon() }, ) Column( modifier = Modifier .padding(horizontal = 16.dp) - .testTag("main_content") + .weight(1f) ) { - Spacer(modifier = Modifier.height(26.dp)) - - Row( - modifier = Modifier - .fillMaxWidth() - .testTag("header_row"), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Headline( - text = AnnotatedString(stringResource(R.string.widgets__blocks__name).spaceToNewline()), - modifier = Modifier.testTag("widget_title"), - ) - Icon( - painter = painterResource(R.drawable.widget_cube), - contentDescription = null, - tint = Color.Unspecified, - modifier = Modifier - .size(64.dp) - .testTag("widget_icon") - ) - } + VerticalSpacer(16.dp) BodyM( text = stringResource(R.string.widgets__blocks__description), color = Colors.White64, - modifier = Modifier - .padding(vertical = 16.dp) - .testTag("widget_description") + modifier = Modifier.testTag("widget_description") ) + VerticalSpacer(16.dp) + HorizontalDivider( modifier = Modifier.testTag("divider") ) SettingsButtonRow( - title = stringResource(R.string.widgets__widget__edit), + title = stringResource(R.string.widgets__widget__settings), value = SettingsButtonValue.StringValue( if (blocksPreferences == BlocksPreferences()) { stringResource(R.string.widgets__widget__edit_default) } else { stringResource(R.string.widgets__widget__edit_custom) - } + }, ), onClick = onClickEdit, modifier = Modifier.testTag("WidgetEdit") ) - Spacer(modifier = Modifier.weight(1f)) - - Text13Up( - stringResource(R.string.common__preview), - color = Colors.White64, - modifier = Modifier - .padding(vertical = 16.dp) - .testTag("preview_label") - ) - block?.let { - BlockCard( + WidgetSizeCarousel( + smallContent = { + BlockCardSmall( + showBlock = blocksPreferences.showBlock, + showTime = blocksPreferences.showTime, + showDate = blocksPreferences.showDate, + showTransactions = blocksPreferences.showTransactions, + showSize = blocksPreferences.showSize, + showFees = blocksPreferences.showFees, + showSource = blocksPreferences.showSource, + block = it.height, + time = it.time, + date = it.date, + transactions = it.transactionCount, + size = it.size, + fees = it.fees, + source = it.source, + modifier = Modifier.testTag("block_card_small") + ) + }, + wideContent = { + BlockCard( + showBlock = blocksPreferences.showBlock, + showTime = blocksPreferences.showTime, + showDate = blocksPreferences.showDate, + showTransactions = blocksPreferences.showTransactions, + showSize = blocksPreferences.showSize, + showFees = blocksPreferences.showFees, + showSource = blocksPreferences.showSource, + block = it.height, + time = it.time, + date = it.date, + transactions = it.transactionCount, + size = it.size, + fees = it.fees, + source = it.source, + modifier = Modifier + .fillMaxWidth() + .testTag("block_card_wide") + ) + }, modifier = Modifier .fillMaxWidth() - .testTag("block_card"), - showWidgetTitle = showWidgetTitles, - showBlock = blocksPreferences.showBlock, - showTime = blocksPreferences.showTime, - showDate = blocksPreferences.showDate, - showTransactions = blocksPreferences.showTransactions, - showSize = blocksPreferences.showSize, - showSource = blocksPreferences.showSource, - block = block.height, - time = block.time, - date = block.date, - transactions = block.transactionCount, - size = block.size, - source = block.source, + .testTag("blocks_preview_carousel") ) } + } - Row( - modifier = Modifier - .padding(vertical = 21.dp) - .fillMaxWidth() - .testTag("buttons_row"), - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - if (isBlocksWidgetEnabled) { - SecondaryButton( - text = stringResource(R.string.common__delete), - modifier = Modifier - .weight(1f) - .testTag("WidgetDelete"), - fullWidth = false, - onClick = onClickDelete - ) - } - - PrimaryButton( - text = stringResource(R.string.common__save), + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + top = 22.dp, + ) + .fillMaxWidth() + .testTag("buttons_row") + ) { + if (isBlocksWidgetEnabled) { + SecondaryButton( + text = stringResource(R.string.common__delete), + fullWidth = false, + onClick = onClickDelete, modifier = Modifier .weight(1f) - .testTag("WidgetSave"), - fullWidth = false, - onClick = onClickSave + .testTag("WidgetDelete") ) } + + PrimaryButton( + text = stringResource(R.string.widgets__widget__save), + fullWidth = false, + onClick = onClickSave, + modifier = Modifier + .weight(1f) + .testTag("WidgetSave") + ) } } } @@ -213,9 +204,8 @@ fun BlocksPreviewContent( @Composable private fun Preview() { AppThemeSurface { - BlocksPreviewContent( + Content( onBack = {}, - showWidgetTitles = true, onClickEdit = {}, onClickDelete = {}, onClickSave = {}, @@ -226,9 +216,10 @@ private fun Preview() { date = "2023-01-01", transactionCount = "2,175", size = "1,606kB", - source = "mempool.space" + source = "mempool.space", + fees = "25 059 357", ), - isBlocksWidgetEnabled = false + isBlocksWidgetEnabled = false, ) } } @@ -237,9 +228,8 @@ private fun Preview() { @Composable private fun Preview2() { AppThemeSurface { - BlocksPreviewContent( + Content( onBack = {}, - showWidgetTitles = false, onClickEdit = {}, onClickDelete = {}, onClickSave = {}, @@ -257,9 +247,10 @@ private fun Preview2() { date = "2023-01-01", transactionCount = "2,175", size = "1,606kB", - source = "mempool.space" + source = "mempool.space", + fees = "25 059 357", ), - isBlocksWidgetEnabled = true + isBlocksWidgetEnabled = true, ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksViewModel.kt index 3f3c0b93ac..70e7415f35 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksViewModel.kt @@ -19,6 +19,7 @@ import to.bitkit.repositories.WidgetsRepo import javax.inject.Inject @HiltViewModel +@Suppress("TooManyFunctions") class BlocksViewModel @Inject constructor( private val widgetsRepo: WidgetsRepo ) : ViewModel() { @@ -99,6 +100,12 @@ class BlocksViewModel @Inject constructor( } } + fun toggleShowFees() { + _customPreferences.update { preferences -> + preferences.copy(showFees = !preferences.showFees) + } + } + fun toggleShowSource() { _customPreferences.update { preferences -> preferences.copy(showSource = !preferences.showSource) diff --git a/app/src/main/res/drawable/ic_cube.xml b/app/src/main/res/drawable/ic_cube.xml new file mode 100644 index 0000000000..4797a6cabd --- /dev/null +++ b/app/src/main/res/drawable/ic_cube.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/app/src/main/res/layout/appwidget_preview_blocks.xml b/app/src/main/res/layout/appwidget_preview_blocks.xml new file mode 100644 index 0000000000..ef5c0ebc74 --- /dev/null +++ b/app/src/main/res/layout/appwidget_preview_blocks.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ffa085d374..3a1213d84f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1140,6 +1140,7 @@ Examine various statistics on newly mined Bitcoin Blocks. Block Date + Fees Size Time Transactions @@ -1174,6 +1175,7 @@ Bitcoin Weather Next block inclusion CONTENT + DATA Widget Feed Custom Default diff --git a/app/src/main/res/xml/appwidget_info_blocks.xml b/app/src/main/res/xml/appwidget_info_blocks.xml new file mode 100644 index 0000000000..6264f6165f --- /dev/null +++ b/app/src/main/res/xml/appwidget_info_blocks.xml @@ -0,0 +1,17 @@ + + diff --git a/changelog.d/next/922.added.md b/changelog.d/next/922.added.md new file mode 100644 index 0000000000..c30858c87e --- /dev/null +++ b/changelog.d/next/922.added.md @@ -0,0 +1 @@ +Bitcoin Blocks home screen widget with v61 wide and compact layouts, including redesigned in-app preview and edit screens