summaryrefslogtreecommitdiff
path: root/app/src/main/java
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-08-31 15:43:02 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-08-31 15:43:02 +0000
commit0cd94d0821661fef3fc34b51011be95034e51a5b (patch)
treeea91f813be8bbe0e5aac506744fcfbb22ce3ad41 /app/src/main/java
parente42bfbd6a65eefd0f24677aebb74993ae56490cf (diff)
Add reader font size setting, rewrite paywall-avoidance settings copy
Font size was hardcoded at 18px in ReaderWebView's injected CSS. Added a DataStore-backed setting (Settings.readerFontSizePx, 14-28px range) with a slider in Settings, threaded through ReaderScreen/ReaderContent into wrapReaderHtml. The reload-tracking key in ReaderContent's AndroidView now includes font size alongside content, so changing it and returning to an already-open article picks up the new size without reloading on every unrelated recomposition (matches the existing comment there about avoiding scroll-position jank). Rewrote the Settings screen's paywall note -- it read as defensive throat-clearing about what the app won't do. Restated as what it's actually for: avoiding tracking, manipulation, and junk fees, with reader mode pulling up anything findable by search anyway. Regenerated the detekt baseline: ReaderContent's pre-existing LongMethod/LongParameterList/CyclomaticComplexMethod findings are keyed by exact function signature text, and adding the fontSizePx parameter changed that text, invalidating the old baseline entries. The underlying debt (a 300+ line, 9-now-10-parameter composable) predates this change and isn't what this commit is about. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/org/terst/feedreader/data/Settings.kt19
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt28
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt11
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/SettingsScreen.kt32
4 files changed, 75 insertions, 15 deletions
diff --git a/app/src/main/java/org/terst/feedreader/data/Settings.kt b/app/src/main/java/org/terst/feedreader/data/Settings.kt
index 2e7eaae..be3e809 100644
--- a/app/src/main/java/org/terst/feedreader/data/Settings.kt
+++ b/app/src/main/java/org/terst/feedreader/data/Settings.kt
@@ -28,8 +28,16 @@ private object SettingsKeys {
val LAST_FETCH_AT = longPreferencesKey("last_fetch_at")
val LAST_FETCH_NEW_ARTICLES = intPreferencesKey("last_fetch_new_articles")
val LAST_ACKNOWLEDGED_FETCH_AT = longPreferencesKey("last_acknowledged_fetch_at")
+ val READER_FONT_SIZE_PX = intPreferencesKey("reader_font_size_px")
}
+// Matches the size ReaderWebView hardcoded before this setting existed, so an upgrading
+// user's reader looks exactly the same until they deliberately change it.
+const val DEFAULT_READER_FONT_SIZE_PX = 18
+private const val READER_FONT_SIZE_MIN_PX = 14
+private const val READER_FONT_SIZE_MAX_PX = 28
+val READER_FONT_SIZE_RANGE_PX = READER_FONT_SIZE_MIN_PX..READER_FONT_SIZE_MAX_PX
+
/** Stash connection settings, configured on the Settings screen. Both blank until set. */
class Settings(private val context: Context) {
val stashBaseUrl: Flow<String> =
@@ -109,4 +117,15 @@ class Settings(private val context: Context) {
suspend fun acknowledgeFetch(at: Long) {
context.settingsDataStore.edit { prefs -> prefs[SettingsKeys.LAST_ACKNOWLEDGED_FETCH_AT] = at }
}
+
+ val readerFontSizePx: Flow<Int> =
+ context.settingsDataStore.data.map { it[SettingsKeys.READER_FONT_SIZE_PX] ?: DEFAULT_READER_FONT_SIZE_PX }
+
+ suspend fun readerFontSizePxNow(): Int = readerFontSizePx.first()
+
+ suspend fun setReaderFontSizePx(px: Int) {
+ context.settingsDataStore.edit { prefs ->
+ prefs[SettingsKeys.READER_FONT_SIZE_PX] = px.coerceIn(READER_FONT_SIZE_RANGE_PX.first, READER_FONT_SIZE_RANGE_PX.last)
+ }
+ }
}
diff --git a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
index 3c3ab66..a89052c 100644
--- a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
@@ -41,6 +41,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -56,6 +57,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.terst.feedreader.data.AppDatabase
import org.terst.feedreader.data.ArticleEntity
+import org.terst.feedreader.data.DEFAULT_READER_FONT_SIZE_PX
import org.terst.feedreader.data.FeedEntity
import org.terst.feedreader.data.Settings
import org.terst.feedreader.data.ensureStandaloneArticle
@@ -99,6 +101,7 @@ fun ReaderScreen(
val db = remember { AppDatabase.get(context) }
val settings = remember { Settings(context) }
val scope = rememberCoroutineScope()
+ val fontSizePx by settings.readerFontSizePx.collectAsState(initial = DEFAULT_READER_FONT_SIZE_PX)
var previousId by remember(articleId) { mutableStateOf<String?>(null) }
var nextId by remember(articleId) { mutableStateOf<String?>(null) }
@@ -156,7 +159,8 @@ fun ReaderScreen(
hasNext = nextId != null,
onPrevious = ::goToPreviousArticle,
onNext = ::goToNextArticle,
- onNextKeepUnread = ::goToNextArticleKeepUnread
+ onNextKeepUnread = ::goToNextArticleKeepUnread,
+ fontSizePx = fontSizePx
)
}
@@ -171,7 +175,8 @@ private fun ReaderContent(
hasNext: Boolean,
onPrevious: () -> Unit,
onNext: () -> Unit,
- onNextKeepUnread: () -> Unit
+ onNextKeepUnread: () -> Unit,
+ fontSizePx: Int
) {
val context = LocalContext.current
val db = remember { AppDatabase.get(context) }
@@ -516,8 +521,11 @@ private fun ReaderContent(
// flipping while a Stash save is in flight. Reloading the WebView's HTML
// on those unrelated recompositions was resetting scroll position mid-swipe,
// which is what made scrolling feel unsmooth/janky. Track what's actually
- // loaded and only reload when the content string itself changes.
- val loadedContent = remember(articleId) { mutableStateOf<String?>(null) }
+ // loaded and only reload when the content string or font size actually
+ // changes -- fontSizePx is included deliberately (not just content) so
+ // changing it in Settings and coming back to an already-open article
+ // picks up the new size, without reloading on every unrelated recomposition.
+ val loadedKey = remember(articleId) { mutableStateOf<Pair<String?, Int>?>(null) }
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { ctx ->
@@ -532,14 +540,16 @@ private fun ReaderContent(
// to 0 on a page that hasn't grown tall enough yet.
onPageFinished = { webView -> webView.scrollTo(0, ReaderScrollPositions.get(articleId)) }
).apply {
- loadDataWithBaseURL(baseUrl, wrapReaderHtml(content, article?.title, article?.publishedAt, article?.imageUrl), "text/html", "UTF-8", null)
- loadedContent.value = content
+ val html = wrapReaderHtml(content, article?.title, article?.publishedAt, article?.imageUrl, fontSizePx)
+ loadDataWithBaseURL(baseUrl, html, "text/html", "UTF-8", null)
+ loadedKey.value = content to fontSizePx
}
},
update = { webView ->
- if (loadedContent.value != content) {
- webView.loadDataWithBaseURL(baseUrl, wrapReaderHtml(content, article?.title, article?.publishedAt, article?.imageUrl), "text/html", "UTF-8", null)
- loadedContent.value = content
+ if (loadedKey.value != content to fontSizePx) {
+ val html = wrapReaderHtml(content, article?.title, article?.publishedAt, article?.imageUrl, fontSizePx)
+ webView.loadDataWithBaseURL(baseUrl, html, "text/html", "UTF-8", null)
+ loadedKey.value = content to fontSizePx
}
},
// AndroidView does not destroy() the underlying WebView on its own --
diff --git a/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt b/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt
index 31fcb02..b13fc58 100644
--- a/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt
@@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
+import org.terst.feedreader.data.DEFAULT_READER_FONT_SIZE_PX
// In-memory only (process lifetime, not persisted to disk) -- scoped to "don't lose my
// place if Next/Previous takes me away and back," not to surviving an app restart. A
@@ -344,7 +345,13 @@ private fun escapeHtml(text: String): String = text
// on whatever heading Readability happened to keep in bodyContent (increasingly nothing --
// see ReaderExtractor's <header> stripping) or on the top app bar's title, which truncates
// to one line and was the only place a long title showed up before this.
-fun wrapReaderHtml(bodyContent: String, title: String? = null, publishedAt: Long? = null, imageUrl: String? = null): String {
+fun wrapReaderHtml(
+ bodyContent: String,
+ title: String? = null,
+ publishedAt: Long? = null,
+ imageUrl: String? = null,
+ fontSizePx: Int = DEFAULT_READER_FONT_SIZE_PX
+): String {
val processedBody = replaceGenericIframes(replaceYouTubeIframes(bodyContent))
// Some feeds' body HTML never embeds an image at all -- the only image they give us
// is a separate RSS media:content/enclosure field (ArticleEntity.imageUrl), which
@@ -369,7 +376,7 @@ fun wrapReaderHtml(bodyContent: String, title: String? = null, publishedAt: Long
return """
<html><head><meta name="viewport" content="width=device-width, initial-scale=1">
<style>
- body { font-family: sans-serif; font-size: 18px; line-height: 1.5; padding: 16px;
+ body { font-family: sans-serif; font-size: ${fontSizePx}px; line-height: 1.5; padding: 16px;
background-color: #121212; color: #e1e1e1; }
h1.article-title { font-size: 1.5em; line-height: 1.3; margin: 0 0 0.6em 0; }
p.article-date { font-size: 0.85em; color: #9aa0a6; margin: -0.4em 0 1em 0; }
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 5f8eca7..966d36b 100644
--- a/app/src/main/java/org/terst/feedreader/ui/SettingsScreen.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/SettingsScreen.kt
@@ -27,6 +27,7 @@ import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
@@ -44,13 +45,16 @@ 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.DEFAULT_READER_FONT_SIZE_PX
import org.terst.feedreader.data.FeedEntity
import org.terst.feedreader.data.FilterAction
import org.terst.feedreader.data.FilterRuleEntity
import org.terst.feedreader.data.Net
+import org.terst.feedreader.data.READER_FONT_SIZE_RANGE_PX
import org.terst.feedreader.data.Settings
import org.terst.feedreader.stash.StashClient
import org.terst.feedreader.stash.StashVerifyResult
+import kotlin.math.roundToInt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -65,9 +69,11 @@ fun SettingsScreen(onBack: () -> Unit) {
var stashKeyVisible by remember { mutableStateOf(false) }
var isVerifyingStash by remember { mutableStateOf(false) }
var stashStatus by remember { mutableStateOf<String?>(null) }
+ var fontSizePx by remember { mutableStateOf(DEFAULT_READER_FONT_SIZE_PX) }
LaunchedEffect(Unit) {
stashUrl = settings.stashBaseUrlNow()
stashKey = settings.stashApiKeyNow()
+ fontSizePx = settings.readerFontSizePxNow()
}
var newRulePattern by remember { mutableStateOf("") }
@@ -98,10 +104,9 @@ fun SettingsScreen(onBack: () -> Unit) {
Text("FeeDreader", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(4.dp))
Text(
- "If a site paywalls its own feed, that's the site's call. But this reader " +
- "won't go out of its way to help a soft paywall block you from reading " +
- "something you already subscribed to get -- reader mode extracts the " +
- "article you were sent, nothing more, nothing sneaky.",
+ "FeeDreader exists to avoid tracking, manipulation, and junk fees. If an " +
+ "article's full text is something you could just as easily find with a " +
+ "search, reader mode will pull it up here too.",
style = MaterialTheme.typography.bodyMedium
)
}
@@ -110,6 +115,25 @@ fun SettingsScreen(onBack: () -> Unit) {
item {
Column(modifier = Modifier.padding(16.dp)) {
+ Text("Reading", style = MaterialTheme.typography.titleMedium)
+ Spacer(modifier = Modifier.height(8.dp))
+ Text("Font size: ${fontSizePx}px", style = MaterialTheme.typography.bodyMedium)
+ Slider(
+ value = fontSizePx.toFloat(),
+ onValueChange = { fontSizePx = it.roundToInt() },
+ valueRange = READER_FONT_SIZE_RANGE_PX.first.toFloat()..READER_FONT_SIZE_RANGE_PX.last.toFloat(),
+ steps = READER_FONT_SIZE_RANGE_PX.last - READER_FONT_SIZE_RANGE_PX.first - 1,
+ // Persist on release, not on every onValueChange -- a drag crosses many
+ // integer values in a fraction of a second, and there's no reason to hit
+ // DataStore that often for a value nothing reads until the next article load.
+ onValueChangeFinished = { scope.launch { settings.setReaderFontSizePx(fontSizePx) } }
+ )
+ }
+ HorizontalDivider()
+ }
+
+ item {
+ Column(modifier = Modifier.padding(16.dp)) {
Text("Stash", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(