Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0a84466
feat: headline card v61 with wide and compact sizes
jvsena42 Apr 28, 2026
1247862
feat: update HeadlinesEditScreen.kt to v61
jvsena42 Apr 29, 2026
6572e0b
Merge branch 'feat/price-widget-v61' into feat/headlines-v61
jvsena42 Apr 29, 2026
49e7dea
feat: update HeadlinesPreviewScreen.kt to v61
jvsena42 Apr 29, 2026
623f348
feat: implement headline OS widget
jvsena42 Apr 29, 2026
690cc32
fix: increase title max lines and fill height for fit in 2 cells
jvsena42 Apr 29, 2026
c371a18
Merge branch 'feat/price-widget-v61' into feat/headlines-v61
jvsena42 Apr 29, 2026
6704f95
chore: private constant
jvsena42 Apr 29, 2026
1d30542
refactor: extract compact widget card size constant
jvsena42 Apr 29, 2026
ce80c09
fix: fill height
jvsena42 Apr 29, 2026
d67a7c0
fix: rotate headlines os widget article each tick
jvsena42 Apr 29, 2026
dc44dba
doc: changelog entry
jvsena42 Apr 29, 2026
4207694
fix: remove drawer button
jvsena42 Apr 29, 2026
8e926e3
fix: guard empty url
jvsena42 Apr 29, 2026
4e2b7cf
refactor: logger call
jvsena42 Apr 29, 2026
b0acba0
refactor: add modifier optional parameter
jvsena42 Apr 29, 2026
cc824a1
Merge branch 'feat/price-widget-v61' into feat/headlines-v61
jvsena42 Apr 29, 2026
2c5999c
Merge remote-tracking branch 'origin/feat/price-widget-v61' into feat…
jvsena42 Apr 30, 2026
729ef56
chore: lint
jvsena42 Apr 30, 2026
b286fa0
refactor: replace raw spacer
jvsena42 Apr 30, 2026
2c55f30
chore: apply modifier rules
jvsena42 Apr 30, 2026
cb54014
chore: lint
jvsena42 Apr 30, 2026
ce66693
Merge branch 'feat/price-widget-v61' into feat/headlines-v61
jvsena42 Apr 30, 2026
0aa64fa
Merge branch 'feat/price-widget-v61' into feat/headlines-v61
jvsena42 Apr 30, 2026
eee104b
fix: ui tests
jvsena42 Apr 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
jvsena42 marked this conversation as resolved.
Outdated
- Home screen widgets foundation with Glance, including price widget as the first implementation #895

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@ class HeadlineCardTest {

@Test
fun testHeadlineCardWithAllElements() {
// Arrange & Act
composeTestRule.setContent {
AppThemeSurface {
HeadlineCard(
showWidgetTitle = true,
showTime = true,
showSource = true,
time = testTime,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -149,55 +100,45 @@ 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
)
}
}

// 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,
Expand All @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ class HeadlinesPreviewContentTest {
onClickEdit = { editClicked = true },
onClickDelete = { deleteClicked = true },
onClickSave = { saveClicked = true },
showWidgetTitles = true,
isHeadlinesImplemented = true,
Comment thread
jvsena42 marked this conversation as resolved.
Comment thread
jvsena42 marked this conversation as resolved.
Comment thread
jvsena42 marked this conversation as resolved.
headlinePreferences = mockHeadlinePreferences,
article = mockArticle
Expand Down Expand Up @@ -97,7 +96,6 @@ class HeadlinesPreviewContentTest {
onClickEdit = { editClicked = true },
onClickDelete = { deleteClicked = true },
onClickSave = { saveClicked = true },
showWidgetTitles = false,
isHeadlinesImplemented = false,
headlinePreferences = mockHeadlinePreferences,
article = mockArticle
Expand Down Expand Up @@ -134,7 +132,6 @@ class HeadlinesPreviewContentTest {
onClickEdit = {},
onClickDelete = {},
onClickSave = {},
showWidgetTitles = true,
isHeadlinesImplemented = true,
headlinePreferences = customPreferences,
article = mockArticle
Expand All @@ -158,7 +155,6 @@ class HeadlinesPreviewContentTest {
onClickEdit = {},
onClickDelete = {},
onClickSave = {},
showWidgetTitles = true,
isHeadlinesImplemented = true,
headlinePreferences = mockHeadlinePreferences,
article = mockArticle
Expand Down Expand Up @@ -194,7 +190,6 @@ class HeadlinesPreviewContentTest {
onClickEdit = {},
onClickDelete = {},
onClickSave = {},
showWidgetTitles = true,
isHeadlinesImplemented = true,
headlinePreferences = mockHeadlinePreferences,
article = mockArticle
Expand All @@ -219,7 +214,6 @@ class HeadlinesPreviewContentTest {
onClickEdit = {},
onClickDelete = {},
onClickSave = {},
showWidgetTitles = false,
isHeadlinesImplemented = false,
headlinePreferences = minimalPreferences,
article = mockArticle
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,19 @@
android:resource="@xml/appwidget_info_price" />
</receiver>

<!-- Headlines Widget -->
<receiver
android:name=".appwidget.ui.headlines.HeadlinesGlanceReceiver"
android:exported="true"
android:label="@string/widgets__news__name">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_info_headlines" />
</receiver>

</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<PriceDTO> =
withContext(ioDispatcher) {
priceService.fetchData(period)
}

suspend fun fetchArticles(): Result<List<ArticleDTO>> =
withContext(ioDispatcher) {
newsService.fetchData()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<ArticleDTO>) {
store.updateData { it.copy(cachedArticles = articles) }
}

suspend fun bumpArticleRotationTick() {
store.updateData { it.copy(articleRotationTick = it.articleRotationTick + 1) }
}
}
13 changes: 13 additions & 0 deletions app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -62,6 +64,7 @@ class AppWidgetRefreshWorker @AssistedInject constructor(

private fun receiverClassFor(type: AppWidgetType): Class<out GlanceAppWidgetReceiver> = when (type) {
AppWidgetType.PRICE -> PriceGlanceReceiver::class.java
AppWidgetType.HEADLINES -> HeadlinesGlanceReceiver::class.java
}
}

Expand All @@ -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)
Comment thread
jvsena42 marked this conversation as resolved.
}
preferencesStore.bumpArticleRotationTick()
HeadlinesGlanceWidget().updateAll(appContext)
}
}
}

Expand Down
Loading
Loading