From 0fc0797fe6c1e5c98a8bc27a3e13cf83a9814927 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 02:09:37 +0000 Subject: [PATCH 1/5] feat: add single-column layout toggle under Style > Feeds page Adds a user preference to force single-column mode on tablets/large screens. When enabled, the 2-pane adaptive layout collapses to a single column where tapping an article takes over the full screen and back returns to the article list. Default is off (adaptive behavior unchanged). - FlowSingleColumnPreference: new boolean preference (ON/OFF) - DataStoreExt: register key "flowSingleColumn" in both PreferencesKey and legacy DataStoreKey - Settings/Preference/SettingsProvider: wire preference through the settings system - AppEntry: override scaffold directive with maxHorizontalPartitions=1 when enabled - FeedsPageStylePage: add "Layout" section with toggle switch - strings.xml: add single_column_layout and layout string resources https://claude.ai/code/session_01DJ46HYUPdGBeDXuYgrqGNB --- .../preference/FlowSingleColumnPreference.kt | 47 +++++++++++++++++++ .../infrastructure/preference/Preference.kt | 1 + .../infrastructure/preference/Settings.kt | 1 + .../preference/SettingsProvider.kt | 1 + .../java/me/ash/reader/ui/ext/DataStoreExt.kt | 5 ++ .../me/ash/reader/ui/page/nav3/AppEntry.kt | 9 +++- .../color/feeds/FeedsPageStylePage.kt | 20 ++++++++ app/src/main/res/values/strings.xml | 2 + 8 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/me/ash/reader/infrastructure/preference/FlowSingleColumnPreference.kt diff --git a/app/src/main/java/me/ash/reader/infrastructure/preference/FlowSingleColumnPreference.kt b/app/src/main/java/me/ash/reader/infrastructure/preference/FlowSingleColumnPreference.kt new file mode 100644 index 000000000..136b8b008 --- /dev/null +++ b/app/src/main/java/me/ash/reader/infrastructure/preference/FlowSingleColumnPreference.kt @@ -0,0 +1,47 @@ +package me.ash.reader.infrastructure.preference + +import android.content.Context +import androidx.compose.runtime.compositionLocalOf +import androidx.datastore.preferences.core.Preferences +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import me.ash.reader.ui.ext.DataStoreKey +import me.ash.reader.ui.ext.DataStoreKey.Companion.flowSingleColumn +import me.ash.reader.ui.ext.dataStore +import me.ash.reader.ui.ext.put + +val LocalFlowSingleColumn = + compositionLocalOf { FlowSingleColumnPreference.default } + +sealed class FlowSingleColumnPreference(val value: Boolean) : Preference() { + object ON : FlowSingleColumnPreference(true) + object OFF : FlowSingleColumnPreference(false) + + override fun put(context: Context, scope: CoroutineScope) { + scope.launch { + context.dataStore.put( + DataStoreKey.flowSingleColumn, + value + ) + } + } + + companion object { + + val default = OFF + val values = listOf(ON, OFF) + + fun fromPreferences(preferences: Preferences) = + when (preferences[DataStoreKey.keys[flowSingleColumn]?.key as Preferences.Key]) { + true -> ON + false -> OFF + else -> default + } + } +} + +operator fun FlowSingleColumnPreference.not(): FlowSingleColumnPreference = + when (value) { + true -> FlowSingleColumnPreference.OFF + false -> FlowSingleColumnPreference.ON + } diff --git a/app/src/main/java/me/ash/reader/infrastructure/preference/Preference.kt b/app/src/main/java/me/ash/reader/infrastructure/preference/Preference.kt index bdc2c057d..7db83a7dc 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/preference/Preference.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/preference/Preference.kt @@ -33,6 +33,7 @@ fun Preferences.toSettings(): Settings { feedsTopBarTonalElevation = FeedsTopBarTonalElevationPreference.fromPreferences(this), feedsGroupListExpand = FeedsGroupListExpandPreference.fromPreferences(this), feedsGroupListTonalElevation = FeedsGroupListTonalElevationPreference.fromPreferences(this), + flowSingleColumn = FlowSingleColumnPreference.fromPreferences(this), // Flow page flowFilterBarStyle = FlowFilterBarStylePreference.fromPreferences(this), diff --git a/app/src/main/java/me/ash/reader/infrastructure/preference/Settings.kt b/app/src/main/java/me/ash/reader/infrastructure/preference/Settings.kt index af4f01012..ad35b7bc6 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/preference/Settings.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/preference/Settings.kt @@ -28,6 +28,7 @@ data class Settings( val feedsTopBarTonalElevation: FeedsTopBarTonalElevationPreference = FeedsTopBarTonalElevationPreference.default, val feedsGroupListExpand: FeedsGroupListExpandPreference = FeedsGroupListExpandPreference.default, val feedsGroupListTonalElevation: FeedsGroupListTonalElevationPreference = FeedsGroupListTonalElevationPreference.default, + val flowSingleColumn: FlowSingleColumnPreference = FlowSingleColumnPreference.default, // Flow page val flowFilterBarStyle: FlowFilterBarStylePreference = FlowFilterBarStylePreference.default, diff --git a/app/src/main/java/me/ash/reader/infrastructure/preference/SettingsProvider.kt b/app/src/main/java/me/ash/reader/infrastructure/preference/SettingsProvider.kt index dd14b72d3..d9cd139d1 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/preference/SettingsProvider.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/preference/SettingsProvider.kt @@ -87,6 +87,7 @@ class SettingsProvider @Inject constructor( LocalFeedsFilterBarStyle provides settings.feedsFilterBarStyle, LocalFeedsFilterBarPadding provides settings.feedsFilterBarPadding, LocalFeedsFilterBarTonalElevation provides settings.feedsFilterBarTonalElevation, + LocalFlowSingleColumn provides settings.flowSingleColumn, // Flow page LocalFlowTopBarTonalElevation provides settings.flowTopBarTonalElevation, diff --git a/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt b/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt index 7552b3d02..4cc54f54c 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt @@ -146,6 +146,7 @@ sealed interface PreferencesKey { const val feedsTopBarTonalElevation = "feedsTopBarTonalElevation" const val feedsGroupListExpand = "feedsGroupListExpand" const val feedsGroupListTonalElevation = "feedsGroupListTonalElevation" + const val flowSingleColumn = "flowSingleColumn" // Flow page const val flowFilterBarStyle = "flowFilterBarStyle" @@ -225,6 +226,7 @@ sealed interface PreferencesKey { IntKey(feedsTopBarTonalElevation), BooleanKey(feedsGroupListExpand), IntKey(feedsGroupListTonalElevation), + BooleanKey(flowSingleColumn), // Flow page IntKey(flowFilterBarStyle), IntKey(flowFilterBarPadding), @@ -308,6 +310,7 @@ data class DataStoreKey(val key: Preferences.Key, val type: Class) { const val feedsTopBarTonalElevation = "feedsTopBarTonalElevation" const val feedsGroupListExpand = "feedsGroupListExpand" const val feedsGroupListTonalElevation = "feedsGroupListTonalElevation" + const val flowSingleColumn = "flowSingleColumn" // Flow page const val flowFilterBarStyle = "flowFilterBarStyle" @@ -404,6 +407,8 @@ data class DataStoreKey(val key: Preferences.Key, val type: Class) { DataStoreKey(booleanPreferencesKey(feedsGroupListExpand), Boolean::class.java), feedsGroupListTonalElevation to DataStoreKey(intPreferencesKey(feedsGroupListTonalElevation), Int::class.java), + flowSingleColumn to + DataStoreKey(booleanPreferencesKey(flowSingleColumn), Boolean::class.java), // Flow page flowFilterBarStyle to DataStoreKey(intPreferencesKey(flowFilterBarStyle), Int::class.java), diff --git a/app/src/main/java/me/ash/reader/ui/page/nav3/AppEntry.kt b/app/src/main/java/me/ash/reader/ui/page/nav3/AppEntry.kt index 0ab9f8349..e5b029a0e 100644 --- a/app/src/main/java/me/ash/reader/ui/page/nav3/AppEntry.kt +++ b/app/src/main/java/me/ash/reader/ui/page/nav3/AppEntry.kt @@ -29,6 +29,7 @@ import me.ash.reader.ui.motion.materialSharedAxisXOut import me.ash.reader.ui.page.adaptive.ArticleData import me.ash.reader.ui.page.adaptive.ArticleListReaderPage import me.ash.reader.ui.page.adaptive.ArticleListReaderViewModel +import me.ash.reader.infrastructure.preference.LocalFlowSingleColumn import me.ash.reader.ui.page.home.feeds.FeedsPage import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel import me.ash.reader.ui.page.nav3.key.Route @@ -69,7 +70,13 @@ fun AppEntry(backStack: NavBackStack) { if (backStack.size == 1) backStack[0] = Route.Feeds else backStack.removeLastOrNull() } - val scaffoldDirective = calculatePaneScaffoldDirective(currentWindowAdaptiveInfo()) + val forceSingleColumn = LocalFlowSingleColumn.current + val computedDirective = calculatePaneScaffoldDirective(currentWindowAdaptiveInfo()) + val scaffoldDirective = if (forceSingleColumn.value) { + computedDirective.copy(maxHorizontalPartitions = 1) + } else { + computedDirective + } val navigator = rememberListDetailPaneScaffoldNavigator( diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/color/feeds/FeedsPageStylePage.kt b/app/src/main/java/me/ash/reader/ui/page/settings/color/feeds/FeedsPageStylePage.kt index 68d71a615..052f8cc7c 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/color/feeds/FeedsPageStylePage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/color/feeds/FeedsPageStylePage.kt @@ -32,6 +32,7 @@ fun FeedsPageStylePage( val topBarTonalElevation = LocalFeedsTopBarTonalElevation.current val groupListExpand = LocalFeedsGroupListExpand.current val groupListTonalElevation = LocalFeedsGroupListTonalElevation.current + val singleColumn = LocalFlowSingleColumn.current val scope = rememberCoroutineScope() @@ -86,6 +87,25 @@ fun FeedsPageStylePage( Spacer(modifier = Modifier.height(24.dp)) } + // Layout + item { + Subtitle( + modifier = Modifier.padding(horizontal = 24.dp), + text = stringResource(R.string.layout) + ) + SettingItem( + title = stringResource(R.string.single_column_layout), + onClick = { + (!singleColumn).put(context, scope) + }, + ) { + RYSwitch(activated = singleColumn.value) { + (!singleColumn).put(context, scope) + } + } + Spacer(modifier = Modifier.height(24.dp)) + } + // Top Bar item { Subtitle( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f7f249f63..7cbe51021 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -187,6 +187,8 @@ Article list Group list Always expand + Single column layout + Layout Top \"Mark as read\" button position Top bar From 3b55fa06abb91b88408c8ff3929be435b2367888 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 05:14:01 +0000 Subject: [PATCH 2/5] Immediately refresh layout when single-column toggle changes The navigator's scaffoldValue only recomputes on navigation events, not when scaffoldDirective alone changes. Without a navigation event, isTwoPane stays stale after toggling, requiring a rotation or app restart to take effect. Add a LaunchedEffect that watches forceSingleColumn via snapshotFlow and navigates to the List pane on every change (after initial composition), triggering the navigator to recompute scaffoldValue with the updated directive. https://claude.ai/code/session_01DJ46HYUPdGBeDXuYgrqGNB --- app/src/main/java/me/ash/reader/ui/page/nav3/AppEntry.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/me/ash/reader/ui/page/nav3/AppEntry.kt b/app/src/main/java/me/ash/reader/ui/page/nav3/AppEntry.kt index e5b029a0e..307476a2c 100644 --- a/app/src/main/java/me/ash/reader/ui/page/nav3/AppEntry.kt +++ b/app/src/main/java/me/ash/reader/ui/page/nav3/AppEntry.kt @@ -14,6 +14,7 @@ import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaf import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator @@ -24,6 +25,7 @@ import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.ui.LocalNavAnimatedContentScope import androidx.navigation3.ui.NavDisplay import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.drop import me.ash.reader.ui.motion.materialSharedAxisXIn import me.ash.reader.ui.motion.materialSharedAxisXOut import me.ash.reader.ui.page.adaptive.ArticleData @@ -84,6 +86,12 @@ fun AppEntry(backStack: NavBackStack) { isDestinationHistoryAware = false, ) + LaunchedEffect(navigator) { + snapshotFlow { forceSingleColumn.value } + .drop(1) + .collect { navigator.navigateTo(ListDetailPaneScaffoldRole.List) } + } + SharedTransitionLayout { NavDisplay( modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surface), From 56fd9087d16c4648bc801d61bf002049a1ef58fd Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 20:25:47 +0000 Subject: [PATCH 3/5] Add CLAUDE.md with project context for Claude Code Covers build setup, architecture overview, preferences system pattern, adaptive layout notes (including the single-column toggle behavior), key libraries, and branch naming convention. https://claude.ai/code/session_01DJ46HYUPdGBeDXuYgrqGNB --- CLAUDE.md | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..868c8a0db --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,96 @@ +# ReadYou — Claude Code Context + +## Project Overview +ReadYou is an Android RSS reader built with Jetpack Compose and Material You (Material 3). It supports multiple RSS/Atom backend sources and presents content in a clean, adaptive layout. + +- **Package:** `me.ash.reader` +- **Min SDK:** 26 | **Target SDK:** 34 | **Compile SDK:** 36 +- **Version:** 0.16.1 (code 46) +- **Language:** Kotlin +- **UI:** Jetpack Compose + Material 3 Adaptive + +## Build & Run + +```bash +# Debug build +./gradlew assembleDebug + +# Run all unit tests +./gradlew test + +# Run instrumented tests +./gradlew connectedAndroidTest + +# Install on connected device +./gradlew installDebug +``` + +Requires JDK 17. If `JAVA_HOME` isn't set, point it at the bundled JBR inside Android Studio: +```bash +export JAVA_HOME=/path/to/Android\ Studio.app/Contents/jbr/Contents/Home +``` + +## Architecture + +**Pattern:** MVVM + Hilt DI + Room + DataStore + +``` +app/src/main/java/me/ash/reader/ +├── infrastructure/ +│ ├── preference/ # All user preferences (DataStore-backed sealed classes) +│ ├── rss/ # RSS sync service, parsers, repositories +│ └── db/ # Room database, DAOs, entities +├── ui/ +│ ├── page/ +│ │ ├── nav3/ # AppEntry.kt — top-level nav scaffold (Nav3 + M3 Adaptive) +│ │ ├── adaptive/ # ArticleListReadingPage — two/one pane scaffold +│ │ ├── home/ +│ │ │ ├── feeds/ # FeedsPage — subscription list +│ │ │ ├── flow/ # FlowPage — article list +│ │ │ └── reading/ # ReadingPage — article reader +│ │ └── settings/ # All settings pages +│ ├── component/ # Shared UI components +│ └── ext/ # DataStore keys, Compose extensions +``` + +## Preferences System + +All preferences follow a consistent pattern: +1. **Sealed class** in `infrastructure/preference/` — extends `Preference`, has `ON`/`OFF` or enum variants, `put()`, `fromPreferences()`, `default`, `LocalXxx` CompositionLocal +2. **DataStore key** registered in `ui/ext/DataStoreExt.kt` — in both `keyList` and the deprecated `DataStoreKey.keys` map +3. **`Settings.kt`** — add field with default +4. **`Preference.kt` `toSettings()`** — map DataStore → Settings +5. **`SettingsProvider.kt` `ProvidesSettings()`** — expose via CompositionLocal + +Use `FlowSingleColumnPreference.kt` as a reference implementation for boolean prefs. + +## Adaptive Layout + +The app uses **Material 3 Adaptive** (`NavigableListDetailPaneScaffold`) for tablet two-pane support. + +Key files: +- `AppEntry.kt` — creates the `ListDetailPaneScaffoldNavigator`, computes `scaffoldDirective` +- `ArticleListReadingPage.kt` — derives `isTwoPane` from `navigator.scaffoldValue` + +**Single-column toggle** (`FlowSingleColumnPreference`): forces `maxHorizontalPartitions = 1` on the directive. A `LaunchedEffect` in `AppEntry.kt` watches the preference via `snapshotFlow` and calls `navigator.navigateTo(List)` on change — this is required because the navigator only recomputes `scaffoldValue` on navigation events, not on directive-only changes. + +## Key Libraries + +| Library | Purpose | +|---------|---------| +| Hilt | Dependency injection | +| Room | Local database | +| DataStore | Preference persistence | +| Nav3 (`androidx.navigation3`) | Navigation back stack | +| Material3 Adaptive | Two-pane / adaptive layout | +| Coil | Image loading | +| OkHttp + Rome | RSS fetching & parsing | +| Timber | Logging | + +## Development Branch + +Active feature work: `claude/rss-reader-layout-research-irv2Q` + +Branch naming convention: `claude/-` + +Push with: `git push -u origin ` From 16e658801af5aa3d0e6b635cc97b956131090f87 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 03:10:54 +0000 Subject: [PATCH 4/5] Allow two-pane layout in landscape when single-column toggle is on When the single-column preference is enabled, only force maxHorizontalPartitions=1 in portrait. In landscape (screenWidthDp > screenHeightDp) the adaptive directive is used as-is, so a tablet rotated sideways naturally shows the two-pane view. https://claude.ai/code/session_01DJ46HYUPdGBeDXuYgrqGNB --- CLAUDE.md | 2 +- app/src/main/java/me/ash/reader/ui/page/nav3/AppEntry.kt | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 868c8a0db..f4703fca7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,7 +72,7 @@ Key files: - `AppEntry.kt` — creates the `ListDetailPaneScaffoldNavigator`, computes `scaffoldDirective` - `ArticleListReadingPage.kt` — derives `isTwoPane` from `navigator.scaffoldValue` -**Single-column toggle** (`FlowSingleColumnPreference`): forces `maxHorizontalPartitions = 1` on the directive. A `LaunchedEffect` in `AppEntry.kt` watches the preference via `snapshotFlow` and calls `navigator.navigateTo(List)` on change — this is required because the navigator only recomputes `scaffoldValue` on navigation events, not on directive-only changes. +**Single-column toggle** (`FlowSingleColumnPreference`): forces `maxHorizontalPartitions = 1` on the directive, but **only in portrait** (`screenWidthDp <= screenHeightDp`). In landscape the adaptive layout runs normally, allowing two panes on a tablet held sideways. A `LaunchedEffect` in `AppEntry.kt` watches the preference via `snapshotFlow` and calls `navigator.navigateTo(List)` on change — this is required because the navigator only recomputes `scaffoldValue` on navigation events, not on directive-only changes. Orientation changes recreate the Activity (no `configChanges` override in the manifest), so no extra effect is needed for rotation. ## Key Libraries diff --git a/app/src/main/java/me/ash/reader/ui/page/nav3/AppEntry.kt b/app/src/main/java/me/ash/reader/ui/page/nav3/AppEntry.kt index 307476a2c..86d94b3eb 100644 --- a/app/src/main/java/me/ash/reader/ui/page/nav3/AppEntry.kt +++ b/app/src/main/java/me/ash/reader/ui/page/nav3/AppEntry.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation3.runtime.NavBackStack @@ -73,8 +74,9 @@ fun AppEntry(backStack: NavBackStack) { } val forceSingleColumn = LocalFlowSingleColumn.current + val isLandscape = LocalConfiguration.current.screenWidthDp > LocalConfiguration.current.screenHeightDp val computedDirective = calculatePaneScaffoldDirective(currentWindowAdaptiveInfo()) - val scaffoldDirective = if (forceSingleColumn.value) { + val scaffoldDirective = if (forceSingleColumn.value && !isLandscape) { computedDirective.copy(maxHorizontalPartitions = 1) } else { computedDirective From 16ffb17b6dd04176bfc23b023b5f2e007c3b76e8 Mon Sep 17 00:00:00 2001 From: Dave <48144766+davezig@users.noreply.github.com> Date: Fri, 21 Aug 2026 20:44:12 -0400 Subject: [PATCH 5/5] Fix mark-as-read-on-scroll never reaching the last screenful (#3) * Fix mark-as-read-on-scroll never reaching the last screenful The last items in the article list had no scroll distance left to pass above the viewport's top edge once the list was fully scrolled, so they could never satisfy the "scrolled past the first visible item" read condition. Add a trailing Spacer sized to the live-measured height of the Box wrapping the LazyColumn (via onSizeChanged), giving the list enough extra scroll range for the last item to reach the same read line as every other item. The spacer is unconditional and independent of the existing FAB/bottom-bar clearance spacers. * Fix trailing articles never being marked read on scroll The trailing spacer added to fix the scroll-boundary bug let the list scroll past the last real article, but once no item with contentType == CONTENT_TYPE_ARTICLE remained visible, firstItemKey became null and the read-detection loop never matched it, so found stayed false and the whole batch of trailing articles was silently discarded instead of committed as read. Treat firstItemKey == null as every remaining article having already scrolled past the read line, matching the same "read line" semantics used for every other item in the list. --------- Co-authored-by: Claude --- .../ash/reader/ui/page/home/flow/FlowPage.kt | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt index 602def14a..e93317858 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt @@ -57,7 +57,9 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -149,6 +151,9 @@ fun FlowPage( val listState = rememberSaveable(pagerData, saver = LazyListState.Saver) { LazyListState(0, 0) } + val density = LocalDensity.current + var viewportHeightPx by remember { mutableStateOf(0) } + val isTopBarElevated = topBarTonalElevation.value > 0 val scrolledTopBarContainerColor = with(MaterialTheme.colorScheme) { if (isTopBarElevated) surfaceContainer else surface } @@ -530,7 +535,11 @@ fun FlowPage( .firstOrNull { it.contentType == CONTENT_TYPE_ARTICLE } ?.key val items = mutableListOf() - var found = false + // No item with contentType == CONTENT_TYPE_ARTICLE remains + // visible once every article has scrolled past the read line + // (e.g. the trailing spacer is the only thing left on screen), + // so treat that as every remaining article being read. + var found = firstItemKey == null val itemCount = pagingItems.itemCount for (index in 0 until itemCount) { pagingItems.peek(index).let { @@ -637,7 +646,12 @@ fun FlowPage( ) .also { currentPullToLoadState = it } - Box(modifier = Modifier.fillMaxSize()) { + Box( + modifier = + Modifier.fillMaxSize().onSizeChanged { + viewportHeightPx = it.height + } + ) { LazyColumn( modifier = Modifier.pullToLoad( @@ -701,6 +715,13 @@ fun FlowPage( WindowInsets.navigationBars ) ) + // Ensures the last item can scroll all the way to the top of the + // viewport, so "mark as read on scroll" can reach it like any + // other item. + Spacer( + modifier = + Modifier.height(with(density) { viewportHeightPx.toDp() }) + ) } } }