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
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/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..499a49a310 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,12 @@ 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) }
+ }
+
+ 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 aaa8d1e95f..ee37fb49cd 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,16 @@ 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)
+ }
+ preferencesStore.bumpArticleRotationTick()
+ 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..1e39f4ccde 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,141 @@ 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,
+ 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,
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..608ad3f976 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,18 @@ 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(),
+ val articleRotationTick: Int = 0,
)
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..e783e6ecc7
--- /dev/null
+++ b/app/src/main/java/to/bitkit/appwidget/ui/headlines/HeadlinesGlanceContent.kt
@@ -0,0 +1,127 @@
+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.HeightModifier
+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 = 4,
+ modifier = GlanceModifier
+ .fillMaxWidth()
+ .then(HeightModifier(Dimension.Expand))
+ )
+
+ val timeVisible = preferences.showTime && article.timeAgo.isNotEmpty()
+ if (!preferences.showSource && !timeVisible) return
+
+ 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()
+ .then(HeightModifier(Dimension.Expand))
+ )
+
+ 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..b05e0ed307
--- /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.articleRotationTick) {
+ data.cachedArticles.randomOrNull()?.toArticleModel()
+ }
+
+ HeadlinesGlanceContent(
+ entry = entry,
+ article = article,
+ )
+ }
+ }
+}
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)
}
}
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/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/components/WidgetSizeCarousel.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/components/WidgetSizeCarousel.kt
new file mode 100644
index 0000000000..f322bffa48
--- /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
+
+private const val PAGE_SMALL = 0
+private const val PAGE_WIDE = 1
+private 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/HeadlineCard.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCard.kt
index 29ba96630d..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
@@ -6,33 +6,23 @@ 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.screens.widgets.components.WidgetCardDimens
import to.bitkit.ui.shared.modifiers.clickableAlpha
import to.bitkit.ui.theme.AppThemeSurface
import to.bitkit.ui.theme.Colors
@@ -40,7 +30,6 @@ import to.bitkit.ui.theme.Colors
@Composable
fun HeadlineCard(
modifier: Modifier = Modifier,
- showWidgetTitle: Boolean = true,
showTime: Boolean = true,
showSource: Boolean = true,
time: String,
@@ -55,70 +44,97 @@ 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)
}
) {
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(WidgetCardDimens.COMPACT_CARD_SIZE)
+ .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)
+ }
+ ) {
+ 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 +144,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 +160,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 +174,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/HeadlinesEditScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesEditScreen.kt
index 0ed17805ed..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
@@ -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,13 +25,12 @@ 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
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
@@ -39,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()
@@ -71,15 +71,17 @@ fun HeadlinesEditContent(
onClickPreview: () -> Unit,
onClickShowSource: () -> Unit,
headlinePreferences: HeadlinePreferences,
- article: ArticleModel
+ 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() },
)
Column(
@@ -87,125 +89,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/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewScreen.kt
index a57a4abdb4..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
@@ -47,7 +38,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 +50,6 @@ fun HeadlinesPreviewScreen(
onBack = onBack,
isHeadlinesImplemented = isHeadlinesImplemented,
headlinePreferences = customHeadlinePreferences,
- showWidgetTitles = showWidgetTitles,
article = article,
onClickEdit = navigateEditWidget,
onClickDelete = {
@@ -80,123 +69,113 @@ fun HeadlinesPreviewContent(
onClickEdit: () -> Unit,
onClickDelete: () -> Unit,
onClickSave: () -> Unit,
- showWidgetTitles: Boolean,
isHeadlinesImplemented: Boolean,
headlinePreferences: HeadlinePreferences,
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(
+ 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"),
- showWidgetTitle = showWidgetTitles,
- showTime = headlinePreferences.showTime,
- showSource = headlinePreferences.showSource,
- time = article.timeAgo,
- headline = article.title,
- source = article.publisher,
- link = article.link
+ .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")
+ )
}
}
}
@@ -207,7 +186,6 @@ private fun Preview() {
AppThemeSurface {
HeadlinesPreviewContent(
onBack = {},
- showWidgetTitles = true,
onClickEdit = {},
onClickDelete = {},
onClickSave = {},
@@ -229,7 +207,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(
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..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,22 +6,15 @@ 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.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 +26,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 +33,11 @@ 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
-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,28 @@ fun PricePreviewContent(
)
if (priceDTO != null) {
- WidgetCarousel(
- pricePreferences = pricePreferences,
- priceDTO = priceDTO,
+ WidgetSizeCarousel(
+ smallContent = {
+ PriceCardSmall(
+ pricePreferences = pricePreferences,
+ priceDTO = priceDTO,
+ modifier = Modifier
+ .size(WidgetCardDimens.COMPACT_CARD_SIZE)
+ .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 +199,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/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/values/strings.xml b/app/src/main/res/values/strings.xml
index 4fbad6e2fc..91e5cdbda3 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1169,11 +1169,14 @@
Find out when it’s a good time to transact on the Bitcoin blockchain.
Bitcoin Weather
Next block inclusion
+ CONTENT
Widget Feed
Custom
Default
Please select which fields you want to display in the {name} widget.
Widget
+ Save Widget
+ Widget Settings
Small
Wide
Source
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 @@
+
+