summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/ui
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-08-24 07:58:47 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-08-24 07:58:47 +0000
commit209da9e628112a5a25ac4519370cfdc072462547 (patch)
treeb88d207bc2f1dafe9a04d4621e9fb9ccd2e3906b /app/src/main/java/org/terst/feedreader/ui
parentda942624a4abd82797f563cf8c57f1124c5de435 (diff)
Let filter rules be scoped to one feed; fix coverage-gate report path bug
The data model and FilterRuleEngine already supported a per-feed scope (FilterRuleEntity.feedId), but Settings' "Add rule" form never let a user set it -- only a hardcoded migration had ever created one. Add a FeedScopePicker so a rule can target "All feeds" (unchanged default) or one specific feed, and show each existing rule's scope in the list. Added the first real unit tests in this repo (FilterRuleEngineTest), covering global vs. feed-scoped matching and mute-wins-over-highlight priority -- the exact behavior this UI gap left unexercised outside one migration-seeded row. First real jacoco coverage number: 2.31%. While verifying, found the pre-commit hook's coverage step was silently a no-op: it pointed at app/build/reports/jacoco/report.xml, but the jacoco setup that landed here uses the standard Gradle JacocoReport task, which writes to reports/jacoco/jacocoTestReport/jacocoTestReport.xml instead. Fixed the path so the gate actually reads real numbers now. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017GMEkHeqKz6FLkmizowBTK
Diffstat (limited to 'app/src/main/java/org/terst/feedreader/ui')
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/SettingsScreen.kt50
1 files changed, 49 insertions, 1 deletions
diff --git a/app/src/main/java/org/terst/feedreader/ui/SettingsScreen.kt b/app/src/main/java/org/terst/feedreader/ui/SettingsScreen.kt
index 3bd6e3e..5f8eca7 100644
--- a/app/src/main/java/org/terst/feedreader/ui/SettingsScreen.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/SettingsScreen.kt
@@ -44,6 +44,7 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import org.terst.feedreader.data.AppDatabase
+import org.terst.feedreader.data.FeedEntity
import org.terst.feedreader.data.FilterAction
import org.terst.feedreader.data.FilterRuleEntity
import org.terst.feedreader.data.Net
@@ -73,6 +74,11 @@ fun SettingsScreen(onBack: () -> Unit) {
var newRuleAction by remember { mutableStateOf(FilterAction.MUTE) }
var actionMenuExpanded by remember { mutableStateOf(false) }
val rules by db.filterRuleDao().observeAll().collectAsState(initial = emptyList())
+ val feeds by db.feedDao().observeAll().collectAsState(initial = emptyList())
+ val feedTitleById = remember(feeds) { feeds.associate { it.id to it.title } }
+ // null = applies to every feed (the only option before this scope picker existed).
+ var newRuleFeedId by remember { mutableStateOf<String?>(null) }
+ var feedMenuExpanded by remember { mutableStateOf(false) }
Scaffold(
topBar = {
@@ -189,6 +195,14 @@ fun SettingsScreen(onBack: () -> Unit) {
}
}
Spacer(modifier = Modifier.height(8.dp))
+ FeedScopePicker(
+ feeds = feeds,
+ selectedFeedId = newRuleFeedId,
+ expanded = feedMenuExpanded,
+ onExpandedChange = { feedMenuExpanded = it },
+ onSelect = { newRuleFeedId = it }
+ )
+ Spacer(modifier = Modifier.height(8.dp))
Button(
enabled = newRulePattern.isNotBlank(),
onClick = {
@@ -197,10 +211,12 @@ fun SettingsScreen(onBack: () -> Unit) {
FilterRuleEntity(
pattern = newRulePattern.trim(),
action = newRuleAction,
+ feedId = newRuleFeedId,
createdAt = System.currentTimeMillis()
)
)
newRulePattern = ""
+ newRuleFeedId = null
}
},
modifier = Modifier.fillMaxWidth()
@@ -211,7 +227,11 @@ fun SettingsScreen(onBack: () -> Unit) {
items(rules, key = { it.id }) { rule ->
ListItem(
headlineContent = { Text(rule.pattern) },
- supportingContent = { Text(if (rule.action == FilterAction.MUTE) "Mute" else "Highlight") },
+ supportingContent = {
+ val actionLabel = if (rule.action == FilterAction.MUTE) "Mute" else "Highlight"
+ val scopeLabel = rule.feedId?.let { feedTitleById[it] ?: it } ?: "All feeds"
+ Text("$actionLabel · $scopeLabel")
+ },
trailingContent = {
IconButton(onClick = { scope.launch { db.filterRuleDao().delete(rule.id) } }) {
Icon(Icons.Default.Delete, contentDescription = "Delete rule")
@@ -223,3 +243,31 @@ fun SettingsScreen(onBack: () -> Unit) {
}
}
}
+
+/** Scope picker for a new filter rule: "All feeds" (feedId = null) or one specific feed. */
+@Composable
+private fun FeedScopePicker(
+ feeds: List<FeedEntity>,
+ selectedFeedId: String?,
+ expanded: Boolean,
+ onExpandedChange: (Boolean) -> Unit,
+ onSelect: (String?) -> Unit
+) {
+ Box {
+ Button(onClick = { onExpandedChange(true) }, modifier = Modifier.fillMaxWidth()) {
+ Text(feeds.find { it.id == selectedFeedId }?.title ?: "All feeds")
+ }
+ DropdownMenu(expanded = expanded, onDismissRequest = { onExpandedChange(false) }) {
+ DropdownMenuItem(
+ text = { Text("All feeds") },
+ onClick = { onSelect(null); onExpandedChange(false) }
+ )
+ feeds.forEach { feed ->
+ DropdownMenuItem(
+ text = { Text(feed.title) },
+ onClick = { onSelect(feed.id); onExpandedChange(false) }
+ )
+ }
+ }
+ }
+}