From 0a84466194abfdc3a87358128f262efae3c4bad8 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 28 Apr 2026 15:20:09 -0300 Subject: [PATCH 01/14] feat: headline card v61 with wide and compact sizes --- .../widgets/headlines/HeadlineCardTest.kt | 106 ++++-------- .../headlines/HeadlinesPreviewContentTest.kt | 6 - .../bitkit/ui/screens/wallets/HomeScreen.kt | 1 - .../screens/widgets/headlines/HeadlineCard.kt | 154 +++++++++++------- .../headlines/HeadlinesPreviewScreen.kt | 14 +- .../widgets/headlines/HeadlinesViewModel.kt | 7 - 6 files changed, 125 insertions(+), 163 deletions(-) diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCardTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCardTest.kt index 3f8a6d3ea9..457eaa3419 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCardTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCardTest.kt @@ -19,11 +19,9 @@ class HeadlineCardTest { @Test fun testHeadlineCardWithAllElements() { - // Arrange & Act composeTestRule.setContent { AppThemeSurface { HeadlineCard( - showWidgetTitle = true, showTime = true, showSource = true, time = testTime, @@ -34,57 +32,21 @@ class HeadlineCardTest { } } - // Assert all elements exist - composeTestRule.onNodeWithTag("widget_title_row", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("widget_title_icon", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("widget_title_text", useUnmergedTree = true).assertExists() composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertExists() composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertExists() composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("source_label", useUnmergedTree = true).assertExists() composeTestRule.onNodeWithTag("source_text", useUnmergedTree = true).assertExists() - // Verify text content composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertTextEquals(testTime) composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertTextEquals(testHeadline) composeTestRule.onNodeWithTag("source_text", useUnmergedTree = true).assertTextEquals(testSource) } - @Test - fun testHeadlineCardWithoutWidgetTitle() { - // Arrange & Act - composeTestRule.setContent { - AppThemeSurface { - HeadlineCard( - showWidgetTitle = false, - showTime = true, - showSource = true, - time = testTime, - headline = testHeadline, - source = testSource, - link = testLink - ) - } - } - - // Assert main elements exist - composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertExists() - - // Assert widget title elements do not exist - composeTestRule.onNodeWithTag("widget_title_row", useUnmergedTree = true).assertDoesNotExist() - composeTestRule.onNodeWithTag("widget_title_icon", useUnmergedTree = true).assertDoesNotExist() - composeTestRule.onNodeWithTag("widget_title_text", useUnmergedTree = true).assertDoesNotExist() - } - @Test fun testHeadlineCardWithoutTime() { - // Arrange & Act composeTestRule.setContent { AppThemeSurface { HeadlineCard( - showWidgetTitle = true, showTime = false, showSource = true, time = testTime, @@ -95,22 +57,18 @@ class HeadlineCardTest { } } - // Assert main elements exist - composeTestRule.onNodeWithTag("widget_title_row", useUnmergedTree = true).assertExists() composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertExists() composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertExists() + composeTestRule.onNodeWithTag("source_text", useUnmergedTree = true).assertExists() - // Assert time element does not exist composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertDoesNotExist() } @Test fun testHeadlineCardWithoutSource() { - // Arrange & Act composeTestRule.setContent { AppThemeSurface { HeadlineCard( - showWidgetTitle = true, showTime = true, showSource = false, time = testTime, @@ -121,24 +79,17 @@ class HeadlineCardTest { } } - // Assert main elements exist - composeTestRule.onNodeWithTag("widget_title_row", useUnmergedTree = true).assertExists() composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertExists() composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertExists() - // Assert source elements do not exist - composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertDoesNotExist() - composeTestRule.onNodeWithTag("source_label", useUnmergedTree = true).assertDoesNotExist() composeTestRule.onNodeWithTag("source_text", useUnmergedTree = true).assertDoesNotExist() } @Test fun testHeadlineCardMinimal() { - // Arrange & Act - Only headline shown composeTestRule.setContent { AppThemeSurface { HeadlineCard( - showWidgetTitle = false, showTime = false, showSource = false, time = testTime, @@ -149,28 +100,23 @@ class HeadlineCardTest { } } - // Assert only essential elements exist composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertExists() - // Assert optional elements do not exist - composeTestRule.onNodeWithTag("widget_title_row", useUnmergedTree = true).assertDoesNotExist() composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertDoesNotExist() composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertDoesNotExist() + composeTestRule.onNodeWithTag("source_text", useUnmergedTree = true).assertDoesNotExist() - // Verify headline text composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertTextEquals(testHeadline) } @Test fun testHeadlineCardWithEmptyTime() { - // Arrange & Act - Time is empty string composeTestRule.setContent { AppThemeSurface { HeadlineCard( - showWidgetTitle = true, showTime = true, showSource = true, - time = "", // Empty time + time = "", headline = testHeadline, source = testSource, link = testLink @@ -178,26 +124,21 @@ class HeadlineCardTest { } } - // Assert main elements exist - composeTestRule.onNodeWithTag("widget_title_row", useUnmergedTree = true).assertExists() composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertExists() composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertExists() + composeTestRule.onNodeWithTag("source_text", useUnmergedTree = true).assertExists() - // Assert time element does not exist when time is empty composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertDoesNotExist() } @Test fun testHeadlineCardWithLongHeadline() { - // Arrange val longHeadline = "This is a very long headline that should be truncated because it exceeds the maximum number of lines allowed in the headline card component and should show ellipsis" - // Act composeTestRule.setContent { AppThemeSurface { HeadlineCard( - showWidgetTitle = true, showTime = true, showSource = true, time = testTime, @@ -208,35 +149,46 @@ class HeadlineCardTest { } } - // Assert headline exists and contains the text (may be truncated) composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertExists() } @Test - fun testAllElementsExistInFullConfiguration() { - // Arrange & Act + fun testHeadlineCardSmallWithTime() { composeTestRule.setContent { AppThemeSurface { - HeadlineCard( - showWidgetTitle = true, + HeadlineCardSmall( showTime = true, - showSource = true, time = testTime, headline = testHeadline, - source = testSource, link = testLink ) } } - // Assert all tagged elements exist - composeTestRule.onNodeWithTag("widget_title_row", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("widget_title_icon", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("widget_title_text", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertExists() composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertExists() composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("source_label", useUnmergedTree = true).assertExists() - composeTestRule.onNodeWithTag("source_text", useUnmergedTree = true).assertExists() + composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertExists() + + composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertTextEquals(testTime) + composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertTextEquals(testHeadline) + } + + @Test + fun testHeadlineCardSmallWithoutTime() { + composeTestRule.setContent { + AppThemeSurface { + HeadlineCardSmall( + showTime = false, + time = testTime, + headline = testHeadline, + link = testLink + ) + } + } + + composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertExists() + + composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertDoesNotExist() + composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertDoesNotExist() } } diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewContentTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewContentTest.kt index a69371fc43..d37dc6c250 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewContentTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewContentTest.kt @@ -42,7 +42,6 @@ class HeadlinesPreviewContentTest { onClickEdit = { editClicked = true }, onClickDelete = { deleteClicked = true }, onClickSave = { saveClicked = true }, - showWidgetTitles = true, isHeadlinesImplemented = true, headlinePreferences = mockHeadlinePreferences, article = mockArticle @@ -97,7 +96,6 @@ class HeadlinesPreviewContentTest { onClickEdit = { editClicked = true }, onClickDelete = { deleteClicked = true }, onClickSave = { saveClicked = true }, - showWidgetTitles = false, isHeadlinesImplemented = false, headlinePreferences = mockHeadlinePreferences, article = mockArticle @@ -134,7 +132,6 @@ class HeadlinesPreviewContentTest { onClickEdit = {}, onClickDelete = {}, onClickSave = {}, - showWidgetTitles = true, isHeadlinesImplemented = true, headlinePreferences = customPreferences, article = mockArticle @@ -158,7 +155,6 @@ class HeadlinesPreviewContentTest { onClickEdit = {}, onClickDelete = {}, onClickSave = {}, - showWidgetTitles = true, isHeadlinesImplemented = true, headlinePreferences = mockHeadlinePreferences, article = mockArticle @@ -194,7 +190,6 @@ class HeadlinesPreviewContentTest { onClickEdit = {}, onClickDelete = {}, onClickSave = {}, - showWidgetTitles = true, isHeadlinesImplemented = true, headlinePreferences = mockHeadlinePreferences, article = mockArticle @@ -219,7 +214,6 @@ class HeadlinesPreviewContentTest { onClickEdit = {}, onClickDelete = {}, onClickSave = {}, - showWidgetTitles = false, isHeadlinesImplemented = false, headlinePreferences = minimalPreferences, article = mockArticle 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 b15c91d270..e7479267e2 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 @@ -767,7 +767,6 @@ private fun Widgets( WidgetType.NEWS -> { homeUiState.currentArticle?.run { HeadlineCard( - showWidgetTitle = homeUiState.showWidgetTitles, showTime = homeUiState.headlinePreferences.showTime, showSource = homeUiState.headlinePreferences.showSource, headline = title, diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCard.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCard.kt index 29ba96630d..e8ac4f7d44 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCard.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCard.kt @@ -6,33 +6,22 @@ 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.height 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.LocalContext import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri -import to.bitkit.R -import to.bitkit.ui.components.BodyM -import to.bitkit.ui.components.BodyMB -import to.bitkit.ui.components.BodyMSB -import to.bitkit.ui.components.BodyS +import to.bitkit.ui.components.BodySSB +import to.bitkit.ui.components.Title import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -40,7 +29,6 @@ import to.bitkit.ui.theme.Colors @Composable fun HeadlineCard( modifier: Modifier = Modifier, - showWidgetTitle: Boolean = true, showTime: Boolean = true, showSource: Boolean = true, time: String, @@ -60,65 +48,90 @@ fun HeadlineCard( } ) { Column( + verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { - if (showWidgetTitle) { + Title( + text = headline, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.testTag("headline_text") + ) + + val timeVisible = showTime && time.isNotEmpty() + if (showSource || timeVisible) { Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.testTag("widget_title_row") + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .testTag("source_row") ) { - Icon( - painter = painterResource(R.drawable.widget_newspaper), - contentDescription = null, - modifier = Modifier - .size(32.dp) - .testTag("widget_title_icon"), - tint = Color.Unspecified - ) - Spacer(modifier = Modifier.width(16.dp)) - BodyMSB( - text = stringResource(R.string.widgets__news__name), - modifier = Modifier.testTag("widget_title_text") - ) + if (showSource) { + BodySSB( + text = source, + color = Colors.Brand, + modifier = Modifier.testTag("source_text") + ) + } + if (timeVisible) { + BodySSB( + text = time, + color = Colors.White64, + modifier = Modifier.testTag("time_text") + ) + } } - Spacer(modifier = Modifier.height(16.dp)) } + } + } +} - if (showTime && time.isNotEmpty()) { - BodyM( - text = time, - modifier = Modifier.testTag("time_text") - ) - Spacer(modifier = Modifier.height(16.dp)) - } +@Composable +fun HeadlineCardSmall( + modifier: Modifier = Modifier, + showTime: Boolean = true, + time: String, + headline: String, + link: String, +) { + val context = LocalContext.current - BodyMB( + Box( + modifier = modifier + .size(width = 163.dp, height = 192.dp) + .clip(shape = MaterialTheme.shapes.medium) + .background(Colors.White10) + .clickableAlpha { + val intent = Intent(Intent.ACTION_VIEW, link.toUri()) + context.startActivity(intent) + } + ) { + Column( + verticalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Title( text = headline, - maxLines = 2, + maxLines = 4, overflow = TextOverflow.Ellipsis, modifier = Modifier.testTag("headline_text") ) - if (showSource) { - Spacer(modifier = Modifier.height(16.dp)) - + if (showTime && time.isNotEmpty()) { Row( + horizontalArrangement = Arrangement.End, modifier = Modifier .fillMaxWidth() - .testTag("source_row"), - horizontalArrangement = Arrangement.SpaceBetween + .testTag("source_row") ) { - BodyS( - text = stringResource(R.string.widgets__widget__source), + BodySSB( + text = time, color = Colors.White64, - modifier = Modifier.testTag("source_label") - ) - BodyS( - text = source, - color = Colors.White64, - modifier = Modifier.testTag("source_text") + modifier = Modifier.testTag("time_text") ) } } @@ -128,13 +141,13 @@ fun HeadlineCard( @Preview(showBackground = true) @Composable -private fun Preview() { +private fun PreviewWide() { AppThemeSurface { Column( verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier .fillMaxSize() - .padding(horizontal = 16.dp) + .padding(16.dp) ) { @Suppress("SpellCheckingInspection") HeadlineCard( @@ -144,7 +157,7 @@ private fun Preview() { link = "" ) HeadlineCard( - showWidgetTitle = false, + showSource = false, time = "21 minutes ago", headline = "How Bitcoin changed El Salvador in more ways a big headline", source = "bitcoinmagazine.com", @@ -158,19 +171,36 @@ private fun Preview() { link = "" ) HeadlineCard( + showTime = false, showSource = false, time = "21 minutes ago", headline = "How Bitcoin changed El Salvador in more ways a big headline", source = "bitcoinmagazine.com", link = "" ) - HeadlineCard( - showWidgetTitle = false, + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewSmall() { + AppThemeSurface { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + HeadlineCardSmall( + time = "21 min ago", + headline = "How Bitcoin changed El Salvador in more ways", + link = "" + ) + HeadlineCardSmall( showTime = false, - showSource = false, - time = "21 minutes ago", - headline = "How Bitcoin changed El Salvador in more ways a big headline", - source = "bitcoinmagazine.com", + time = "21 min ago", + headline = "How Bitcoin changed El Salvador", link = "" ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewScreen.kt index a57a4abdb4..91ffc0ee49 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewScreen.kt @@ -47,7 +47,6 @@ fun HeadlinesPreviewScreen( onBack: () -> Unit, navigateEditWidget: () -> Unit, ) { - val showWidgetTitles by headlinesViewModel.showWidgetTitles.collectAsStateWithLifecycle() val customHeadlinePreferences by headlinesViewModel.customPreferences.collectAsStateWithLifecycle() val article by headlinesViewModel.currentArticle.collectAsStateWithLifecycle() val isHeadlinesImplemented by headlinesViewModel.isNewsWidgetEnabled.collectAsStateWithLifecycle() @@ -60,7 +59,6 @@ fun HeadlinesPreviewScreen( onBack = onBack, isHeadlinesImplemented = isHeadlinesImplemented, headlinePreferences = customHeadlinePreferences, - showWidgetTitles = showWidgetTitles, article = article, onClickEdit = navigateEditWidget, onClickDelete = { @@ -80,7 +78,6 @@ fun HeadlinesPreviewContent( onClickEdit: () -> Unit, onClickDelete: () -> Unit, onClickSave: () -> Unit, - showWidgetTitles: Boolean, isHeadlinesImplemented: Boolean, headlinePreferences: HeadlinePreferences, article: ArticleModel, @@ -158,16 +155,15 @@ fun HeadlinesPreviewContent( ) HeadlineCard( - modifier = Modifier - .fillMaxWidth() - .testTag("headline_card"), - showWidgetTitle = showWidgetTitles, showTime = headlinePreferences.showTime, showSource = headlinePreferences.showSource, time = article.timeAgo, headline = article.title, source = article.publisher, - link = article.link + link = article.link, + modifier = Modifier + .fillMaxWidth() + .testTag("headline_card") ) Row( @@ -207,7 +203,6 @@ private fun Preview() { AppThemeSurface { HeadlinesPreviewContent( onBack = {}, - showWidgetTitles = true, onClickEdit = {}, onClickDelete = {}, onClickSave = {}, @@ -229,7 +224,6 @@ private fun Preview2() { AppThemeSurface { HeadlinesPreviewContent( onBack = {}, - showWidgetTitles = false, onClickEdit = {}, onClickDelete = {}, onClickSave = {}, diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesViewModel.kt index 14f389e93b..6ee64e0091 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesViewModel.kt @@ -43,13 +43,6 @@ class HeadlinesViewModel @Inject constructor( initialValue = false ) - val showWidgetTitles: StateFlow = widgetsRepo.showWidgetTitles - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(SUBSCRIPTION_TIMEOUT), - initialValue = true - ) - val currentArticle: StateFlow = widgetsRepo.articlesFlow.map { articles -> articles.map { it.toArticleModel() }.randomOrNull() ?: DEFAULT_ARTICLE }.stateIn( From 1247862a454c5b89dee72e0d3381b5f0f185e8ba Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 29 Apr 2026 07:20:46 -0300 Subject: [PATCH 02/14] feat: update HeadlinesEditScreen.kt to v61 --- .../widgets/headlines/HeadlinesEditScreen.kt | 100 +++++++++--------- app/src/main/res/values/strings.xml | 1 + 2 files changed, 50 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesEditScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesEditScreen.kt index 0ed17805ed..611873c4fc 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesEditScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesEditScreen.kt @@ -1,5 +1,6 @@ package to.bitkit.ui.screens.widgets.headlines +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -24,8 +25,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.models.widget.ArticleModel import to.bitkit.models.widget.HeadlinePreferences -import to.bitkit.ui.components.BodyM -import to.bitkit.ui.components.CaptionB +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 @@ -74,10 +75,13 @@ fun HeadlinesEditContent( article: ArticleModel ) { ScreenColumn( - modifier = Modifier.testTag("headlines_edit_screen") + noBackground = true, + modifier = Modifier + .background(Colors.Gray7) + .testTag("headlines_edit_screen") ) { AppTopBar( - titleText = stringResource(R.string.widgets__widget__edit), + titleText = stringResource(R.string.widgets__news__name), onBackClick = onBack, actions = { DrawerNavIcon() }, ) @@ -87,125 +91,119 @@ fun HeadlinesEditContent( .padding(horizontal = 16.dp) .testTag("WidgetEditScrollView") ) { - Spacer(modifier = Modifier.height(26.dp)) + Spacer(modifier = Modifier.height(16.dp)) - BodyM( - text = stringResource(R.string.widgets__widget__edit_description).replace( - "{name}", - stringResource(R.string.widgets__news__name) - ), + Caption13Up( + text = stringResource(R.string.widgets__widget__content), color = Colors.White64, - modifier = Modifier.testTag("edit_description") + modifier = Modifier + .padding(bottom = 16.dp) + .testTag("content_section_header") ) - Spacer(modifier = Modifier.height(32.dp)) - Row( - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .padding(vertical = 21.dp) + .padding(vertical = 8.dp) .fillMaxWidth() - .testTag("time_setting_row") + .testTag("title_setting_row") ) { - BodyM( - text = article.timeAgo, - modifier = Modifier.testTag("time_text") + Title( + text = article.title, + modifier = Modifier + .weight(1f) + .testTag("title_text") ) IconButton( - onClick = onClickTime, - modifier = Modifier.testTag("time_toggle_button") + onClick = {}, + enabled = false, + modifier = Modifier.testTag("title_toggle_button") ) { Icon( painter = painterResource(R.drawable.ic_checkmark), contentDescription = null, - tint = if (headlinePreferences.showTime) Colors.Brand else Colors.White50, + tint = Colors.Brand, modifier = Modifier .size(32.dp) - .testTag("time_toggle_icon"), + .testTag("title_toggle_icon"), ) } } HorizontalDivider( - modifier = Modifier.testTag("time_divider") + modifier = Modifier.testTag("title_divider") ) Row( horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .padding(vertical = 21.dp) + .padding(vertical = 8.dp) .fillMaxWidth() - .testTag("title_setting_row") + .testTag("source_setting_row") ) { - Title( - text = article.title, + BodySSB( + text = article.publisher, + color = Colors.Brand, modifier = Modifier .weight(1f) - .testTag("title_text") + .testTag("source_text") ) IconButton( - onClick = {}, - enabled = false, - modifier = Modifier.testTag("title_toggle_button") + onClick = onClickShowSource, + modifier = Modifier.testTag("source_toggle_button") ) { Icon( painter = painterResource(R.drawable.ic_checkmark), contentDescription = null, - tint = Colors.Brand, + tint = if (headlinePreferences.showSource) Colors.Brand else Colors.White50, modifier = Modifier .size(32.dp) - .testTag("title_toggle_icon"), + .testTag("source_toggle_icon"), ) } } HorizontalDivider( - modifier = Modifier.testTag("title_divider") + modifier = Modifier.testTag("source_divider") ) Row( horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .padding(vertical = 21.dp) + .padding(vertical = 8.dp) .fillMaxWidth() - .testTag("source_setting_row") + .testTag("time_setting_row") ) { - CaptionB( - text = stringResource(R.string.widgets__widget__source), + BodySSB( + text = article.timeAgo, color = Colors.White64, modifier = Modifier .weight(1f) - .testTag("source_label") - ) - - CaptionB( - text = article.publisher, - color = Colors.White64, - modifier = Modifier.testTag("source_text") + .testTag("time_text") ) IconButton( - onClick = onClickShowSource, - modifier = Modifier.testTag("source_toggle_button") + onClick = onClickTime, + modifier = Modifier.testTag("time_toggle_button") ) { Icon( painter = painterResource(R.drawable.ic_checkmark), contentDescription = null, - tint = if (headlinePreferences.showSource) Colors.Brand else Colors.White50, + tint = if (headlinePreferences.showTime) Colors.Brand else Colors.White50, modifier = Modifier .size(32.dp) - .testTag("source_toggle_icon"), + .testTag("time_toggle_icon"), ) } } HorizontalDivider( - modifier = Modifier.testTag("source_divider") + modifier = Modifier.testTag("time_divider") ) Spacer(modifier = Modifier.weight(1f)) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4fbad6e2fc..0911b05480 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1169,6 +1169,7 @@ Find out when it’s a good time to transact on the Bitcoin blockchain. Bitcoin Weather Next block inclusion + CONTENT Widget Feed Custom Default From 49e7deaa3f14df5a3eb7b6baec981c779d47b6fd Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 29 Apr 2026 07:37:32 -0300 Subject: [PATCH 03/14] feat: update HeadlinesPreviewScreen.kt to v61 --- .../widgets/components/WidgetSizeCarousel.kt | 98 ++++++++++++ .../headlines/HeadlinesPreviewScreen.kt | 151 ++++++++---------- .../widgets/price/PricePreviewScreen.kt | 120 +++----------- app/src/main/res/values/strings.xml | 2 + 4 files changed, 189 insertions(+), 182 deletions(-) create mode 100644 app/src/main/java/to/bitkit/ui/screens/widgets/components/WidgetSizeCarousel.kt diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/components/WidgetSizeCarousel.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/components/WidgetSizeCarousel.kt new file mode 100644 index 0000000000..bd8495b068 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/components/WidgetSizeCarousel.kt @@ -0,0 +1,98 @@ +package to.bitkit.ui.screens.widgets.components + +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import to.bitkit.R +import to.bitkit.ui.components.Caption13Up +import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.theme.Colors + +const val PAGE_SMALL = 0 +const val PAGE_WIDE = 1 +const val PAGE_COUNT = 2 + +@Composable +fun WidgetSizeCarousel( + modifier: Modifier = Modifier, + smallContent: @Composable () -> Unit, + wideContent: @Composable () -> Unit, +) { + val pagerState = rememberPagerState(pageCount = { PAGE_COUNT }) + + Column( + verticalArrangement = Arrangement.Center, + modifier = modifier.testTag("widget_size_carousel") + ) { + HorizontalPager( + state = pagerState, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .testTag("widget_size_pager") + ) { page -> + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth() + ) { + when (page) { + PAGE_SMALL -> smallContent() + PAGE_WIDE -> wideContent() + } + } + } + + VerticalSpacer(16.dp) + + Caption13Up( + text = stringResource( + if (pagerState.currentPage == PAGE_SMALL) { + R.string.widgets__widget__size_small + } else { + R.string.widgets__widget__size_wide + }, + ), + color = Colors.White64, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .testTag("widget_size_label") + ) + + VerticalSpacer(16.dp) + + Row( + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .testTag("page_indicator") + ) { + repeat(PAGE_COUNT) { index -> + Box( + modifier = Modifier + .padding(horizontal = 4.dp) + .size(8.dp) + .background( + color = if (pagerState.currentPage == index) Colors.White else Colors.White32, + shape = CircleShape, + ), + ) + } + } + } +} diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewScreen.kt index 91ffc0ee49..f7d9e2e005 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewScreen.kt @@ -1,42 +1,33 @@ package to.bitkit.ui.screens.widgets.headlines +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.ArticleModel import to.bitkit.models.widget.HeadlinePreferences 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 @@ -83,116 +74,108 @@ fun HeadlinesPreviewContent( article: ArticleModel, ) { ScreenColumn( - modifier = Modifier.testTag("headlines_preview_screen") + noBackground = true, + modifier = Modifier + .background(Colors.Gray7) + .testTag("headlines_preview_screen") ) { AppTopBar( - titleText = stringResource(R.string.widgets__widget__nav_title), + titleText = stringResource(R.string.widgets__news__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__news__name).spaceToNewline()), - modifier = Modifier.testTag("widget_title"), - ) - Icon( - painter = painterResource(R.drawable.widget_newspaper), - contentDescription = null, - tint = Color.Unspecified, - modifier = Modifier - .size(64.dp) - .testTag("widget_icon") - ) - } + VerticalSpacer(16.dp) BodyM( text = stringResource(R.string.widgets__news__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 (headlinePreferences == HeadlinePreferences()) { 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") - ) - - HeadlineCard( - showTime = headlinePreferences.showTime, - showSource = headlinePreferences.showSource, - time = article.timeAgo, - headline = article.title, - source = article.publisher, - link = article.link, + WidgetSizeCarousel( + smallContent = { + HeadlineCardSmall( + showTime = headlinePreferences.showTime, + time = article.timeAgo, + headline = article.title, + link = article.link, + modifier = Modifier.testTag("headline_card_small") + ) + }, + wideContent = { + HeadlineCard( + showTime = headlinePreferences.showTime, + showSource = headlinePreferences.showSource, + time = article.timeAgo, + headline = article.title, + source = article.publisher, + link = article.link, + modifier = Modifier + .fillMaxWidth() + .testTag("headline_card_wide") + ) + }, modifier = Modifier .fillMaxWidth() - .testTag("headline_card") + .testTag("headlines_preview_carousel") ) + } - Row( - modifier = Modifier - .padding(vertical = 21.dp) - .fillMaxWidth() - .testTag("buttons_row"), - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - if (isHeadlinesImplemented) { - 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 (isHeadlinesImplemented) { + 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") + ) } } } diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt index f334a524d4..da9eb7e63e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt @@ -8,20 +8,14 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.HorizontalDivider 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.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -33,7 +27,6 @@ import to.bitkit.data.dto.price.PriceWidgetData import to.bitkit.data.dto.price.TradingPair import to.bitkit.models.widget.PricePreferences import to.bitkit.ui.components.BodyM -import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.VerticalSpacer @@ -41,13 +34,10 @@ 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.ScreenColumn +import to.bitkit.ui.screens.widgets.components.WidgetSizeCarousel import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -private const val PAGE_SMALL = 0 -private const val PAGE_WIDE = 1 -private const val PAGE_COUNT = 2 - @Composable fun PricePreviewScreen( priceViewModel: PriceViewModel, @@ -145,11 +135,29 @@ fun PricePreviewContent( ) if (priceDTO != null) { - WidgetCarousel( - pricePreferences = pricePreferences, - priceDTO = priceDTO, + WidgetSizeCarousel( + smallContent = { + PriceCardSmall( + pricePreferences = pricePreferences, + priceDTO = priceDTO, + modifier = Modifier + .width(163.dp) + .height(192.dp) + .testTag("price_card_small") + ) + }, + wideContent = { + PriceCard( + pricePreferences = pricePreferences, + priceDTO = priceDTO, + modifier = Modifier + .fillMaxWidth() + .testTag("price_card_wide") + ) + }, modifier = Modifier .fillMaxWidth() + .testTag("price_preview_carousel") ) } else { Box(modifier = Modifier.weight(1f)) @@ -192,90 +200,6 @@ fun PricePreviewContent( } } -@Composable -private fun WidgetCarousel( - modifier: Modifier = Modifier, - pricePreferences: PricePreferences, - priceDTO: PriceDTO, -) { - val pagerState = rememberPagerState(pageCount = { PAGE_COUNT }) - - Column( - verticalArrangement = Arrangement.Center, - modifier = modifier.testTag("price_preview_carousel") - ) { - HorizontalPager( - state = pagerState, - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .testTag("price_preview_pager") - ) { page -> - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxWidth() - ) { - when (page) { - PAGE_SMALL -> PriceCardSmall( - pricePreferences = pricePreferences, - priceDTO = priceDTO, - modifier = Modifier - .width(163.dp) - .height(192.dp) - .testTag("price_card_small") - ) - - PAGE_WIDE -> PriceCard( - pricePreferences = pricePreferences, - priceDTO = priceDTO, - modifier = Modifier - .fillMaxWidth() - .testTag("price_card_wide") - ) - } - } - } - - VerticalSpacer(16.dp) - - Caption13Up( - text = stringResource( - if (pagerState.currentPage == PAGE_SMALL) { - R.string.widgets__widget__size_small - } else { - R.string.widgets__widget__size_wide - }, - ), - color = Colors.White64, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .testTag("widget_size_label") - ) - - VerticalSpacer(16.dp) - - Row( - horizontalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxWidth() - .testTag("page_indicator") - ) { - repeat(PAGE_COUNT) { index -> - Box( - modifier = Modifier - .padding(horizontal = 4.dp) - .size(8.dp) - .background( - color = if (pagerState.currentPage == index) Colors.White else Colors.White32, - shape = CircleShape, - ), - ) - } - } - } -} - @Preview(showBackground = true) @Composable private fun Preview() { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0911b05480..91e5cdbda3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1175,6 +1175,8 @@ Default Please select which fields you want to display in the {name} widget. Widget + Save Widget + Widget Settings Small Wide Source From 623f348e98ba5b234816669cd5dc8dcd253f7436 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 29 Apr 2026 08:18:42 -0300 Subject: [PATCH 04/14] feat: implement headline OS widget --- app/src/main/AndroidManifest.xml | 13 ++ .../appwidget/AppWidgetDataRepository.kt | 8 + .../appwidget/AppWidgetPreferencesStore.kt | 5 + .../appwidget/AppWidgetRefreshWorker.kt | 12 ++ .../config/AppWidgetConfigActivity.kt | 26 ++- .../appwidget/config/AppWidgetConfigScreen.kt | 148 ++++++++++++++++++ .../config/AppWidgetConfigViewModel.kt | 77 +++++++-- .../appwidget/model/AppWidgetPreferences.kt | 11 ++ .../ui/headlines/HeadlinesGlanceContent.kt | 123 +++++++++++++++ .../ui/headlines/HeadlinesGlanceReceiver.kt | 40 +++++ .../ui/headlines/HeadlinesGlanceWidget.kt | 46 ++++++ .../layout/appwidget_preview_headlines.xml | 47 ++++++ .../main/res/xml/appwidget_info_headlines.xml | 17 ++ 13 files changed, 559 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceReceiver.kt create mode 100644 app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceWidget.kt create mode 100644 app/src/main/res/layout/appwidget_preview_headlines.xml create mode 100644 app/src/main/res/xml/appwidget_info_headlines.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ef4b31e795..1b02cdd87b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -204,6 +204,19 @@ android:resource="@xml/appwidget_info_price" /> + + + + + + + + diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetDataRepository.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetDataRepository.kt index 1bbbcd6b4e..40b5e9c6ee 100644 --- a/app/src/main/java/to/bitkit/appwidget/AppWidgetDataRepository.kt +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetDataRepository.kt @@ -2,8 +2,10 @@ package to.bitkit.appwidget import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext +import to.bitkit.data.dto.ArticleDTO import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.PriceDTO +import to.bitkit.data.widgets.NewsService import to.bitkit.data.widgets.PriceService import to.bitkit.di.IoDispatcher import javax.inject.Inject @@ -13,9 +15,15 @@ import javax.inject.Singleton class AppWidgetDataRepository @Inject constructor( @IoDispatcher private val ioDispatcher: CoroutineDispatcher, private val priceService: PriceService, + private val newsService: NewsService, ) { suspend fun fetchPriceData(period: GraphPeriod = GraphPeriod.ONE_DAY): Result = withContext(ioDispatcher) { priceService.fetchData(period) } + + suspend fun fetchArticles(): Result> = + withContext(ioDispatcher) { + newsService.fetchData() + } } diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt index 88b8e865ce..2ccbd4297b 100644 --- a/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.map 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.price.GraphPeriod import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.serializers.AppWidgetDataSerializer @@ -79,4 +80,8 @@ class AppWidgetPreferencesStore @Inject constructor( suspend fun cachePriceData(period: GraphPeriod, price: PriceDTO) { store.updateData { it.copy(cachedPrices = it.cachedPrices + (period to price)) } } + + suspend fun cacheArticles(articles: List) { + store.updateData { it.copy(cachedArticles = articles) } + } } diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt index aaa8d1e95f..9b5f64c3e4 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.headlines.HeadlinesGlanceReceiver +import to.bitkit.appwidget.ui.headlines.HeadlinesGlanceWidget import to.bitkit.appwidget.ui.price.PriceGlanceReceiver import to.bitkit.appwidget.ui.price.PriceGlanceWidget import to.bitkit.utils.Logger @@ -62,6 +64,7 @@ class AppWidgetRefreshWorker @AssistedInject constructor( private fun receiverClassFor(type: AppWidgetType): Class = when (type) { AppWidgetType.PRICE -> PriceGlanceReceiver::class.java + AppWidgetType.HEADLINES -> HeadlinesGlanceReceiver::class.java } } @@ -84,6 +87,15 @@ class AppWidgetRefreshWorker @AssistedInject constructor( } PriceGlanceWidget().updateAll(appContext) } + + AppWidgetType.HEADLINES -> { + dataRepository.fetchArticles() + .onSuccess { preferencesStore.cacheArticles(it) } + .onFailure { + Logger.warn("Failed to refresh headlines", it, context = TAG) + } + HeadlinesGlanceWidget().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 049d383e21..83b5a1dcde 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,9 @@ 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.headlines.HeadlinesGlanceReceiver +import to.bitkit.appwidget.ui.headlines.HeadlinesGlanceWidget +import to.bitkit.appwidget.ui.price.PriceGlanceReceiver import to.bitkit.appwidget.ui.price.PriceGlanceWidget import to.bitkit.ui.theme.AppThemeSurface @@ -38,9 +41,7 @@ class AppWidgetConfigActivity : ComponentActivity() { return } - val typeName = intent?.getStringExtra(EXTRA_WIDGET_TYPE) - val type = typeName?.let { runCatching { AppWidgetType.valueOf(it) }.getOrNull() } - ?: AppWidgetType.PRICE + val type = resolveWidgetType(appWidgetId) viewModel.init(appWidgetId, type) @@ -49,7 +50,10 @@ class AppWidgetConfigActivity : ComponentActivity() { AppWidgetConfigScreen( viewModel = viewModel, onConfirm = { - PriceGlanceWidget().updateAll(this@AppWidgetConfigActivity) + when (viewModel.uiState.value.type) { + AppWidgetType.PRICE -> PriceGlanceWidget().updateAll(this@AppWidgetConfigActivity) + AppWidgetType.HEADLINES -> HeadlinesGlanceWidget().updateAll(this@AppWidgetConfigActivity) + } AppWidgetRefreshWorker.enqueue(this@AppWidgetConfigActivity) val result = Intent().putExtra( AppWidgetManager.EXTRA_APPWIDGET_ID, @@ -63,4 +67,18 @@ class AppWidgetConfigActivity : ComponentActivity() { } } } + + private fun resolveWidgetType(appWidgetId: Int): AppWidgetType { + val extraType = intent?.getStringExtra(EXTRA_WIDGET_TYPE) + ?.let { runCatching { AppWidgetType.valueOf(it) }.getOrNull() } + if (extraType != null) return extraType + + val providerClass = AppWidgetManager.getInstance(this) + .getAppWidgetInfo(appWidgetId)?.provider?.className + return when (providerClass) { + HeadlinesGlanceReceiver::class.java.name -> AppWidgetType.HEADLINES + PriceGlanceReceiver::class.java.name -> AppWidgetType.PRICE + else -> AppWidgetType.PRICE + } + } } 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 7629dbdf62..a2434d3d63 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt @@ -5,6 +5,7 @@ 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 @@ -12,6 +13,7 @@ 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 @@ -24,11 +26,14 @@ 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.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 @@ -52,6 +57,15 @@ fun AppWidgetConfigScreen( onSave = { viewModel.saveAndFinish(onConfirm) }, onCancel = onCancel, ) + + AppWidgetType.HEADLINES -> HeadlinesConfigContent( + state = state, + onToggleSource = { viewModel.toggleShowSource() }, + onToggleTime = { viewModel.toggleShowTime() }, + onReset = { viewModel.resetPreferences() }, + onSave = { viewModel.saveAndFinish(onConfirm) }, + onCancel = onCancel, + ) } } @@ -139,6 +153,140 @@ private fun PriceConfigContent( } } +@Composable +private fun HeadlinesConfigContent( + state: AppWidgetConfigUiState, + onToggleSource: () -> Unit, + onToggleTime: () -> Unit, + onReset: () -> Unit, + onSave: () -> Unit, + onCancel: () -> Unit, +) { + val prefs = state.headlinePreferences + val previewArticle = ArticleModel( + title = "How Bitcoin changed El Salvador in more ways", + timeAgo = "21 minutes ago", + publisher = "bitcoinmagazine.com", + link = "", + ) + + 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, + 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, 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 9d36446b6e..393404cb75 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigViewModel.kt @@ -13,9 +13,11 @@ 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.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.HeadlinePreferences import to.bitkit.models.widget.PricePreferences import to.bitkit.utils.Logger import javax.inject.Inject @@ -42,6 +44,7 @@ class AppWidgetConfigViewModel @Inject constructor( appWidgetId = appWidgetId, type = type, pricePreferences = entry?.pricePreferences?.toInApp() ?: PricePreferences(), + headlinePreferences = entry?.headlinePreferences?.toInApp() ?: HeadlinePreferences(), ) } } @@ -59,27 +62,70 @@ class AppWidgetConfigViewModel @Inject constructor( } } + fun toggleShowTime() { + _uiState.update { + it.copy( + headlinePreferences = it.headlinePreferences.copy(showTime = !it.headlinePreferences.showTime), + ) + } + } + + fun toggleShowSource() { + _uiState.update { + it.copy( + headlinePreferences = it.headlinePreferences.copy(showSource = !it.headlinePreferences.showSource), + ) + } + } + fun resetPreferences() { - _uiState.update { it.copy(pricePreferences = PricePreferences()) } + _uiState.update { + when (it.type) { + AppWidgetType.PRICE -> it.copy(pricePreferences = PricePreferences()) + AppWidgetType.HEADLINES -> it.copy(headlinePreferences = HeadlinePreferences()) + } + } } fun saveAndFinish(onComplete: suspend () -> Unit) { viewModelScope.launch { - val appWidgetId = _uiState.value.appWidgetId - val pricePreferences = _uiState.value.pricePreferences + val state = _uiState.value _uiState.update { it.copy(isSaving = true) } - preferencesStore.registerWidget(appWidgetId, AppWidgetType.PRICE) - preferencesStore.updateEntry(appWidgetId) { entry -> - entry.copy(pricePreferences = pricePreferences.toHome()) + + when (state.type) { + AppWidgetType.PRICE -> savePrice(state) + AppWidgetType.HEADLINES -> saveHeadlines(state) } - val period = pricePreferences.period ?: GraphPeriod.ONE_DAY - dataRepository.fetchPriceData(period) - .onSuccess { preferencesStore.cachePriceData(period, it) } - .onFailure { Logger.warn("Failed to fetch initial price data", it, context = TAG) } + onComplete() _uiState.update { it.copy(isSaving = false) } } } + + private suspend fun savePrice(state: AppWidgetConfigUiState) { + val appWidgetId = state.appWidgetId + val pricePreferences = state.pricePreferences + preferencesStore.registerWidget(appWidgetId, AppWidgetType.PRICE) + preferencesStore.updateEntry(appWidgetId) { entry -> + entry.copy(pricePreferences = pricePreferences.toHome()) + } + val period = pricePreferences.period ?: GraphPeriod.ONE_DAY + dataRepository.fetchPriceData(period) + .onSuccess { preferencesStore.cachePriceData(period, it) } + .onFailure { Logger.warn("Failed to fetch initial price data", it, context = TAG) } + } + + private suspend fun saveHeadlines(state: AppWidgetConfigUiState) { + val appWidgetId = state.appWidgetId + val headlinePreferences = state.headlinePreferences + preferencesStore.registerWidget(appWidgetId, AppWidgetType.HEADLINES) + preferencesStore.updateEntry(appWidgetId) { entry -> + entry.copy(headlinePreferences = headlinePreferences.toHome()) + } + dataRepository.fetchArticles() + .onSuccess { preferencesStore.cacheArticles(it) } + .onFailure { Logger.warn("Failed to fetch initial articles", it, context = TAG) } + } } @Stable @@ -87,6 +133,7 @@ data class AppWidgetConfigUiState( val appWidgetId: Int = -1, val type: AppWidgetType = AppWidgetType.PRICE, val pricePreferences: PricePreferences = PricePreferences(), + val headlinePreferences: HeadlinePreferences = HeadlinePreferences(), val isSaving: Boolean = false, ) @@ -99,3 +146,13 @@ private fun PricePreferences.toHome() = HomePricePreferences( enabledPairs = enabledPairs, period = period ?: GraphPeriod.ONE_DAY, ) + +private fun HomeHeadlinePreferences.toInApp() = HeadlinePreferences( + showTime = showTime, + showSource = showSource, +) + +private fun HeadlinePreferences.toHome() = HomeHeadlinePreferences( + showTime = showTime, + showSource = showSource, +) 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 0aedb2ed04..dc14acf251 100644 --- a/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt +++ b/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt @@ -2,12 +2,14 @@ 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.price.GraphPeriod import to.bitkit.data.dto.price.PriceDTO import to.bitkit.data.dto.price.TradingPair enum class AppWidgetType { PRICE, + HEADLINES, } @Stable @@ -16,6 +18,7 @@ data class AppWidgetEntry( val appWidgetId: Int, val type: AppWidgetType, val pricePreferences: HomePricePreferences = HomePricePreferences(), + val headlinePreferences: HomeHeadlinePreferences = HomeHeadlinePreferences(), ) @Stable @@ -25,9 +28,17 @@ data class HomePricePreferences( val period: GraphPeriod = GraphPeriod.ONE_DAY, ) +@Stable +@Serializable +data class HomeHeadlinePreferences( + val showTime: Boolean = true, + val showSource: Boolean = true, +) + @Stable @Serializable data class AppWidgetData( val entries: List = emptyList(), val cachedPrices: Map = emptyMap(), + val cachedArticles: List = emptyList(), ) 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 new file mode 100644 index 0000000000..94453de302 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt @@ -0,0 +1,123 @@ +package to.bitkit.appwidget.ui.headlines + +import android.appwidget.AppWidgetManager +import android.content.Intent +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import androidx.glance.GlanceModifier +import androidx.glance.LocalContext +import androidx.glance.LocalSize +import androidx.glance.color.ColorProvider +import androidx.glance.layout.Alignment +import androidx.glance.layout.Row +import androidx.glance.layout.WidthModifier +import androidx.glance.layout.fillMaxWidth +import androidx.glance.text.Text +import androidx.glance.unit.Dimension +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.HomeHeadlinePreferences +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.VerticalSpacer +import to.bitkit.appwidget.ui.theme.GlanceColors +import to.bitkit.appwidget.ui.theme.GlanceTextStyles +import to.bitkit.models.widget.ArticleModel +import to.bitkit.ui.theme.Colors + +@Suppress("RestrictedApi") +@Composable +fun HeadlinesGlanceContent( + entry: AppWidgetEntry, + article: ArticleModel?, +) { + val context = LocalContext.current + val tapIntent = if (article != null && article.link.isNotEmpty()) { + Intent(Intent.ACTION_VIEW, article.link.toUri()).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + } else { + 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) + } + } + + GlanceWidgetScaffold(onClick = tapIntent) { + if (article == null) { + CaptionB(text = context.getString(R.string.appwidget__loading)) + return@GlanceWidgetScaffold + } + + if (LocalSize.current.width >= GlanceLayoutDimens.WIDE_LAYOUT_MIN_WIDTH) { + WideContent(article = article, preferences = entry.headlinePreferences) + } else { + CompactContent(article = article, preferences = entry.headlinePreferences) + } + } +} + +@Suppress("RestrictedApi") +@Composable +private fun WideContent(article: ArticleModel, preferences: HomeHeadlinePreferences) { + Text( + text = article.title, + style = GlanceTextStyles.title22, + maxLines = 2, + modifier = GlanceModifier.fillMaxWidth() + ) + + val timeVisible = preferences.showTime && article.timeAgo.isNotEmpty() + if (!preferences.showSource && !timeVisible) return + + VerticalSpacer(16.dp) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = GlanceModifier.fillMaxWidth() + ) { + if (preferences.showSource) { + BodySSB( + text = article.publisher, + color = ColorProvider(day = Colors.Brand, night = Colors.Brand), + modifier = if (timeVisible) GlanceModifier.then(WidthModifier(Dimension.Expand)) else GlanceModifier + ) + } + if (timeVisible) { + BodySSB( + text = article.timeAgo, + color = GlanceColors.textSecondary + ) + } + } +} + +@Suppress("RestrictedApi") +@Composable +private fun CompactContent(article: ArticleModel, preferences: HomeHeadlinePreferences) { + Text( + text = article.title, + style = GlanceTextStyles.title22, + maxLines = 4, + modifier = GlanceModifier.fillMaxWidth() + ) + + val timeVisible = preferences.showTime && article.timeAgo.isNotEmpty() + if (!timeVisible) return + + VerticalSpacer(16.dp) + Row( + horizontalAlignment = Alignment.End, + modifier = GlanceModifier.fillMaxWidth() + ) { + BodySSB( + text = article.timeAgo, + color = GlanceColors.textSecondary + ) + } +} diff --git a/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceReceiver.kt b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceReceiver.kt new file mode 100644 index 0000000000..ec0fc4ebf7 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceReceiver.kt @@ -0,0 +1,40 @@ +package to.bitkit.appwidget.ui.headlines + +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 HeadlinesGlanceReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget: GlanceAppWidget = HeadlinesGlanceWidget() + + 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/headlines/HeadlinesGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceWidget.kt new file mode 100644 index 0000000000..0e7b81de99 --- /dev/null +++ b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceWidget.kt @@ -0,0 +1,46 @@ +package to.bitkit.appwidget.ui.headlines + +import android.content.Context +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +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.appwidget.ui.components.GlanceLayoutDimens +import to.bitkit.models.widget.toArticleModel + +class HeadlinesGlanceWidget : GlanceAppWidget() { + + override val sizeMode = SizeMode.Responsive( + setOf(GlanceLayoutDimens.COMPACT_WIDGET_SIZE, GlanceLayoutDimens.WIDE_WIDGET_SIZE), + ) + + 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.HEADLINES) + val article = remember(data.cachedArticles) { + data.cachedArticles.firstOrNull()?.toArticleModel() + } + + HeadlinesGlanceContent( + entry = entry, + article = article, + ) + } + } +} diff --git a/app/src/main/res/layout/appwidget_preview_headlines.xml b/app/src/main/res/layout/appwidget_preview_headlines.xml new file mode 100644 index 0000000000..46ef515c77 --- /dev/null +++ b/app/src/main/res/layout/appwidget_preview_headlines.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/appwidget_info_headlines.xml b/app/src/main/res/xml/appwidget_info_headlines.xml new file mode 100644 index 0000000000..e10bf14eb7 --- /dev/null +++ b/app/src/main/res/xml/appwidget_info_headlines.xml @@ -0,0 +1,17 @@ + + From 690cc32e08a53d1d218ee83538bd86d0cdefce15 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 29 Apr 2026 08:28:36 -0300 Subject: [PATCH 05/14] fix: increase title max lines and fill height for fit in 2 cells --- .../appwidget/ui/headlines/HeadlinesGlanceContent.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 94453de302..2908a76ccd 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 @@ -10,6 +10,7 @@ import androidx.glance.LocalContext import androidx.glance.LocalSize import androidx.glance.color.ColorProvider import androidx.glance.layout.Alignment +import androidx.glance.layout.HeightModifier import androidx.glance.layout.Row import androidx.glance.layout.WidthModifier import androidx.glance.layout.fillMaxWidth @@ -69,14 +70,15 @@ private fun WideContent(article: ArticleModel, preferences: HomeHeadlinePreferen Text( text = article.title, style = GlanceTextStyles.title22, - maxLines = 2, - modifier = GlanceModifier.fillMaxWidth() + maxLines = 4, + modifier = GlanceModifier + .fillMaxWidth() + .then(HeightModifier(Dimension.Expand)) ) val timeVisible = preferences.showTime && article.timeAgo.isNotEmpty() if (!preferences.showSource && !timeVisible) return - VerticalSpacer(16.dp) Row( verticalAlignment = Alignment.CenterVertically, modifier = GlanceModifier.fillMaxWidth() From 6704f959867cadadd3ec2bc4f404529d9c8b4027 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 29 Apr 2026 10:24:10 -0300 Subject: [PATCH 06/14] chore: private constant --- .../ui/screens/widgets/components/WidgetSizeCarousel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/components/WidgetSizeCarousel.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/components/WidgetSizeCarousel.kt index bd8495b068..f322bffa48 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/components/WidgetSizeCarousel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/components/WidgetSizeCarousel.kt @@ -23,9 +23,9 @@ import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.theme.Colors -const val PAGE_SMALL = 0 -const val PAGE_WIDE = 1 -const val PAGE_COUNT = 2 +private const val PAGE_SMALL = 0 +private const val PAGE_WIDE = 1 +private const val PAGE_COUNT = 2 @Composable fun WidgetSizeCarousel( From 1d305424e118b13357bf1d3374a94d3411292efb Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 29 Apr 2026 10:31:56 -0300 Subject: [PATCH 07/14] refactor: extract compact widget card size constant --- .../ui/screens/widgets/components/WidgetCardDimens.kt | 8 ++++++++ .../bitkit/ui/screens/widgets/headlines/HeadlineCard.kt | 3 ++- .../bitkit/ui/screens/widgets/price/PricePreviewScreen.kt | 7 +++---- 3 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/to/bitkit/ui/screens/widgets/components/WidgetCardDimens.kt diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/components/WidgetCardDimens.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/components/WidgetCardDimens.kt new file mode 100644 index 0000000000..95a43be9fe --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/components/WidgetCardDimens.kt @@ -0,0 +1,8 @@ +package to.bitkit.ui.screens.widgets.components + +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp + +object WidgetCardDimens { + val COMPACT_CARD_SIZE = DpSize(163.dp, 192.dp) +} diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCard.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCard.kt index e8ac4f7d44..6c48e7fd9a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCard.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCard.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.Title +import to.bitkit.ui.screens.widgets.components.WidgetCardDimens import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -100,7 +101,7 @@ fun HeadlineCardSmall( Box( modifier = modifier - .size(width = 163.dp, height = 192.dp) + .size(WidgetCardDimens.COMPACT_CARD_SIZE) .clip(shape = MaterialTheme.shapes.medium) .background(Colors.White10) .clickableAlpha { diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt index da9eb7e63e..1a9105e1c9 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt @@ -6,9 +6,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.size import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -34,6 +33,7 @@ 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.ScreenColumn +import to.bitkit.ui.screens.widgets.components.WidgetCardDimens import to.bitkit.ui.screens.widgets.components.WidgetSizeCarousel import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -141,8 +141,7 @@ fun PricePreviewContent( pricePreferences = pricePreferences, priceDTO = priceDTO, modifier = Modifier - .width(163.dp) - .height(192.dp) + .size(WidgetCardDimens.COMPACT_CARD_SIZE) .testTag("price_card_small") ) }, From ce80c0973fa96d6db874c4767d099f841fcd0681 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 29 Apr 2026 10:40:57 -0300 Subject: [PATCH 08/14] fix: fill height --- .../bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 2908a76ccd..e783e6ecc7 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 @@ -106,7 +106,9 @@ private fun CompactContent(article: ArticleModel, preferences: HomeHeadlinePrefe text = article.title, style = GlanceTextStyles.title22, maxLines = 4, - modifier = GlanceModifier.fillMaxWidth() + modifier = GlanceModifier + .fillMaxWidth() + .then(HeightModifier(Dimension.Expand)) ) val timeVisible = preferences.showTime && article.timeAgo.isNotEmpty() From d67a7c03a68325260c4b5573b6188718ce917cec Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 29 Apr 2026 11:02:24 -0300 Subject: [PATCH 09/14] fix: rotate headlines os widget article each tick --- .../java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt | 4 ++++ .../main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt | 1 + .../java/to/bitkit/appwidget/model/AppWidgetPreferences.kt | 1 + .../to/bitkit/appwidget/ui/headlines/HeadlinesGlanceWidget.kt | 4 ++-- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt index 2ccbd4297b..499a49a310 100644 --- a/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt @@ -84,4 +84,8 @@ class AppWidgetPreferencesStore @Inject constructor( suspend fun cacheArticles(articles: List) { store.updateData { it.copy(cachedArticles = articles) } } + + suspend fun bumpArticleRotationTick() { + store.updateData { it.copy(articleRotationTick = it.articleRotationTick + 1) } + } } diff --git a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt index 9b5f64c3e4..ee37fb49cd 100644 --- a/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt +++ b/app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt @@ -94,6 +94,7 @@ class AppWidgetRefreshWorker @AssistedInject constructor( .onFailure { Logger.warn("Failed to refresh headlines", it, context = TAG) } + preferencesStore.bumpArticleRotationTick() HeadlinesGlanceWidget().updateAll(appContext) } } 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 dc14acf251..608ad3f976 100644 --- a/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt +++ b/app/src/main/java/to/bitkit/appwidget/model/AppWidgetPreferences.kt @@ -41,4 +41,5 @@ data class AppWidgetData( val entries: List = emptyList(), val cachedPrices: Map = emptyMap(), val cachedArticles: List = emptyList(), + val articleRotationTick: Int = 0, ) diff --git a/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceWidget.kt b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceWidget.kt index 0e7b81de99..b05e0ed307 100644 --- a/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceWidget.kt +++ b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceWidget.kt @@ -33,8 +33,8 @@ class HeadlinesGlanceWidget : GlanceAppWidget() { val data by store.data.collectAsState(initial = AppWidgetData()) val entry = data.entries.find { it.appWidgetId == appWidgetId } ?: AppWidgetEntry(appWidgetId = appWidgetId, type = AppWidgetType.HEADLINES) - val article = remember(data.cachedArticles) { - data.cachedArticles.firstOrNull()?.toArticleModel() + val article = remember(data.cachedArticles, data.articleRotationTick) { + data.cachedArticles.randomOrNull()?.toArticleModel() } HeadlinesGlanceContent( From dc44dbab99230d0d298edb67158c937290e742fd Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 29 Apr 2026 11:24:11 -0300 Subject: [PATCH 10/14] doc: changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ecbc8842b..b9f9aaf810 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Polish Terms of Use screen padding to match iOS #903 ### Added +- Headlines home screen widget with v61 wide and compact layouts, including redesigned in-app preview and edit screens #919 - Home screen widgets foundation with Glance, including price widget as the first implementation #895 ### Changed From 420769464db422b1dbaf2a9576139255473613ee Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 29 Apr 2026 11:26:01 -0300 Subject: [PATCH 11/14] fix: remove drawer button --- .../ui/screens/widgets/headlines/HeadlinesEditScreen.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesEditScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesEditScreen.kt index 611873c4fc..91cb5ee4ce 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesEditScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesEditScreen.kt @@ -31,7 +31,6 @@ import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.Title 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 @@ -40,7 +39,7 @@ import to.bitkit.ui.theme.Colors fun HeadlinesEditScreen( headlinesViewModel: HeadlinesViewModel, onBack: () -> Unit, - navigatePreview: () -> Unit + navigatePreview: () -> Unit, ) { val customHeadlinePreferences by headlinesViewModel.customPreferences.collectAsStateWithLifecycle() val article by headlinesViewModel.currentArticle.collectAsStateWithLifecycle() @@ -72,7 +71,7 @@ fun HeadlinesEditContent( onClickPreview: () -> Unit, onClickShowSource: () -> Unit, headlinePreferences: HeadlinePreferences, - article: ArticleModel + article: ArticleModel, ) { ScreenColumn( noBackground = true, @@ -83,7 +82,6 @@ fun HeadlinesEditContent( AppTopBar( titleText = stringResource(R.string.widgets__news__name), onBackClick = onBack, - actions = { DrawerNavIcon() }, ) Column( From 8e926e359f08940f49e8e8a5ea41834dacb85c0a Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 29 Apr 2026 13:27:33 -0300 Subject: [PATCH 12/14] fix: guard empty url --- .../java/to/bitkit/ui/screens/widgets/headlines/HeadlineCard.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCard.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCard.kt index 6c48e7fd9a..8b8329a002 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCard.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCard.kt @@ -44,6 +44,7 @@ fun HeadlineCard( .clip(shape = MaterialTheme.shapes.medium) .background(Colors.White10) .clickableAlpha { + if (link.isEmpty()) return@clickableAlpha val intent = Intent(Intent.ACTION_VIEW, link.toUri()) context.startActivity(intent) } @@ -105,6 +106,7 @@ fun HeadlineCardSmall( .clip(shape = MaterialTheme.shapes.medium) .background(Colors.White10) .clickableAlpha { + if (link.isEmpty()) return@clickableAlpha val intent = Intent(Intent.ACTION_VIEW, link.toUri()) context.startActivity(intent) } From 4e2b7cffc0baa033f48b4adf9bcb6a6844a54555 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 29 Apr 2026 13:33:43 -0300 Subject: [PATCH 13/14] refactor: logger call --- app/src/main/java/to/bitkit/data/widgets/NewsService.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/widgets/NewsService.kt b/app/src/main/java/to/bitkit/data/widgets/NewsService.kt index 108381eb77..f9e1de5cd6 100644 --- a/app/src/main/java/to/bitkit/data/widgets/NewsService.kt +++ b/app/src/main/java/to/bitkit/data/widgets/NewsService.kt @@ -24,14 +24,11 @@ class NewsService @Inject constructor( override suspend fun fetchData(): Result> = runCatching { get>(Env.newsBaseUrl + "/articles").take(10) - }.onFailure { - Logger.warn(e = it, msg = "Failed to fetch news", context = TAG) } - // Future services can be added here private suspend inline fun get(url: String): T { val response: HttpResponse = client.get(url) - Logger.debug("Http call: $response") + Logger.verbose("Http call: $response", context = TAG) return when (response.status.isSuccess()) { true -> { val responseBody = runCatching { response.body() }.getOrElse { @@ -39,6 +36,7 @@ class NewsService @Inject constructor( } responseBody } + else -> throw NewsError.InvalidResponse(response.status.description) } } From b0acba0c26933d63f80741581964c7a87abcf034 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 29 Apr 2026 13:36:20 -0300 Subject: [PATCH 14/14] refactor: add modifier optional parameter --- .../java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 a2434d3d63..1e39f4ccde 100644 --- a/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt +++ b/app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigScreen.kt @@ -263,12 +263,13 @@ 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 + modifier = modifier .padding(vertical = 8.dp) .fillMaxWidth() ) {