Sharing one ScrollState across every row in the diff panels made each row's
layout pass overwrite the shared max-scroll bound with its own line width,
so the visible offset snapped back and forth mid-drag ("글자가 춤을 추네").
Replace it with a manually-driven offset clamped against one bound computed
once per file, instead of recomputed per row.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
80 lines
12 KiB
Markdown
80 lines
12 KiB
Markdown
# TASK-025: Tabbed branch detail screen (Files + History), repository row commit time, swipeable repository/favorites tabs, and live-testing follow-up fixes
|
|
|
|
Status: implemented; all required checks pass; device verification pending
|
|
Owner: Claude
|
|
Reviewer: Codex
|
|
|
|
## Outcome
|
|
|
|
Three more live requests from the same testing session:
|
|
|
|
1. Tapping a branch/tag now opens one screen with two swipeable tabs — **파일** (Files, the code browser, default) and **히스토리** (History) — over the same ref, restoring file/folder browsing (removed from the UI by TASK-024) while keeping the direct-to-history behavior the human product owner asked for, by making history the second tab instead of the only destination.
|
|
2. Each repository row on the repository list now shows its last-commit time (reusing the existing `updatedAtEpochMillis` field and the shared `formatInstant` helper from TASK-024 — no new API data needed).
|
|
3. The repository list's 전체(All)/즐겨찾기(Favorites) tabs are now swipeable (swipe right from All → Favorites, swipe left from Favorites → All), not just tap-to-switch.
|
|
|
|
Plus six more follow-up fixes/changes found or requested during the same live-testing pass on the same branch, kept in this task rather than split out since they're all small, tightly related touch-ups to screens this task already modifies or reuses:
|
|
|
|
4. **Overlap bug fix**: the narrow-layout (non-Z-Fold-unfolded) branch of `CommitDetailScreen`'s `BoxWithConstraints` had `Header(...)` and its content as bare siblings instead of children of a `Column`, so `BoxWithConstraints` (which behaves like `Box` — it does not arrange multi-child branches vertically on its own) rendered them stacked on top of each other. Reported twice by the human product owner via screenshot before being root-caused; fixed by wrapping each branch's contents in `Column(Modifier.fillMaxSize()) { ... }`.
|
|
5. **Status-bar padding fix**: `BranchDetailScreen`'s `TabRow` was jammed against the status bar because, unlike every other top-level screen in the app, it wasn't wrapped in its own `Scaffold`. Fixed with `Modifier.statusBarsPadding()` on the outer `Column` (not a nested `Scaffold`, to avoid double-padding since `CodeBrowserScreen`/`HistoryScreen` already have their own `Scaffold`s internally).
|
|
6. **Diff horizontal-scroll fix, in two passes**: `DiffSideLine` (in the History tab's file-diff view) originally had `Modifier.fillMaxWidth().horizontalScroll(state)` and no `softWrap = false`, so long lines wrapped instead of overflowing — nothing to scroll. First pass fixed that by removing `fillMaxWidth()` and adding `softWrap = false`, matching the already-working `PatchText` composable's modifier pattern — but that pattern shares one `ScrollState` across every row in the panel (so dragging any line moves them all together), and a single `ScrollState`'s scroll bound is set from whichever row's layout pass ran most recently; with many differently-wide lines in the same list, that bound kept getting overwritten mid-drag, snapping the visible offset back and forth ("글자가 춤을 추네" — reported live, with a screenshot, after the first pass had already been installed). Second pass replaced the shared `ScrollState` with a hand-rolled scroll: one `Modifier.scrollable(...)` per row feeding a single shared offset value, clamped against one bound computed once up front (via `TextMeasurer`, from the longest line in the file) instead of recomputed per row, then applied to each row with a plain `Modifier.offset { ... }` — so no per-row layout pass can stomp on another row's bound anymore.
|
|
7. **Loading-text simplification**: the references-loading message on the repository screen previously interpolated the repository name into a longer sentence; simplified to a plain "로딩 중" per direct request, and the now-unused `%1$s` placeholder removed from `repositories_references_loading`.
|
|
8. **In-app back buttons removed**: every screen-header back button (`CodeBrowserScreen`'s `BrowserHeader`, `HistoryScreen`'s `Header`, `NotificationsScreen`'s `FilterTabs`, `WorkItemsScreen`'s `ModeTabs` and `DetailHeader`, `RepositoryListScreen`'s `ReferencesContent` header) removed, since every screen already has a `BackHandler` wired to the system back gesture and the human product owner asked to rely on that instead. `WorkItemsScreen`'s `DetailHeader` composable (which consisted only of a back button) removed entirely along with all 5 call sites. Now-unused `history_back`/`notifications_back`/`workitems_back`/`codebrowser_back` string resources removed; `repositories_back` kept, since it's a distinct recovery action on the references-error state's secondary button, not a header back button.
|
|
9. **Pull-to-refresh replaces manual refresh buttons**: `RepositoryListScreen`'s `RepositoryContent` and `ReferencesContent` each had a manual "새로고침" `OutlinedButton`; both replaced with `PullToRefreshBox` (Material3, `@OptIn(ExperimentalMaterial3Api::class)`) wrapping their `LazyColumn`s, so pulling down from the top refreshes instead.
|
|
|
|
## Scope
|
|
|
|
- Included: `ui/navigation/Route.kt`/`RouteStackSaver.kt` (`Route.CodeBrowser`/`Route.History` replaced by one `Route.BranchDetail`), new `feature/branchdetail/BranchDetailScreen.kt`, `MainActivity.kt` wiring, `strings_branchdetail.xml`, plus the follow-up touch-ups (items 4-9 above) to `feature/history/HistoryScreen.kt`, `feature/codebrowser/CodeBrowserScreen.kt`, `feature/notifications/NotificationsScreen.kt`, `feature/workitems/WorkItemsScreen.kt`, `feature/repositories/RepositoryListScreen.kt`, and their string resources.
|
|
- Excluded: No change to `CodeBrowserViewModel`/`HistoryViewModel` internals — both are reused exactly as they were, including their own internal drill-down (subfolder navigation, commit detail) and their own per-state `BackHandler`s. This screen only adds the `TabRow` + `HorizontalPager` chrome around them. No change to actual refresh/pagination/network logic — item 9 only changes the trigger UI, not what `onRefresh` does.
|
|
|
|
## Design notes
|
|
|
|
- Both tabs' ViewModels (`codeBrowserViewModel`, `historyViewModel`) are opened together when `Route.BranchDetail` becomes current, and closed together when either tab's internal back-handling has nothing left to unwind (`onCodeBrowserBack`/`onHistoryBack` both close both ViewModels and pop the route) — a single shared exit point rather than two independently-tracked one.
|
|
- `HorizontalPager`'s default `beyondViewportPageCount` (0) means only the currently visible tab is composed, so each tab's own internal `BackHandler`s never conflict with each other — the system back button always applies to whichever tab is actually on screen, with no extra coordination code needed.
|
|
- `CodeBrowserScreen`'s existing "히스토리" header button is kept, repurposed to animate the pager to the History tab instead of pushing a route — a working existing affordance, not dead code.
|
|
- `Route.CodeBrowser` and `Route.History` are fully removed (not left dead), since `Route.BranchDetail` is now the only way either screen is reached.
|
|
|
|
## Acceptance criteria
|
|
|
|
- [x] Tapping a branch/tag opens `Route.BranchDetail`, defaulting to the Files tab.
|
|
- [x] Files tab supports folder drill-down and reading file contents (the restored TASK-006 code browser, unmodified).
|
|
- [x] Swiping left/right between tabs works (`HorizontalPager`); tapping a tab also works (`TabRow`).
|
|
- [x] System back: within a subfolder/commit-detail, back unwinds that tab's own internal state first; only at each tab's own root does back exit the branch-detail screen entirely.
|
|
- [x] `RouteEncoding`/`RouteStackSaver` round-trip `Route.BranchDetail` including a ref containing a slash, and fall back to `Repositories` on truncated/malformed persisted data.
|
|
- [x] Commit-detail screen renders correctly (no overlapping content) in the narrow/folded layout.
|
|
- [x] `BranchDetailScreen`'s tab row sits below the status bar, not jammed against it.
|
|
- [x] Diff view supports horizontal scroll on long lines (synced across before/after panels).
|
|
- [x] References-loading message reads a plain "로딩 중".
|
|
- [x] No in-app back buttons remain on any screen header; system back gesture still exits each screen/subview correctly.
|
|
- [x] Pulling down from the top of the repository list or references list triggers a refresh; no manual refresh button remains.
|
|
- [x] Build, unit tests, and lint pass.
|
|
- [ ] Full device confirmation of the tab UI, swipe gesture, back-button removal, and pull-to-refresh together in one pass.
|
|
|
|
## Required checks
|
|
|
|
```text
|
|
./gradlew assembleDebug
|
|
./gradlew testDebugUnitTest
|
|
./gradlew lintDebug
|
|
```
|
|
|
|
## Evidence
|
|
|
|
- `feature/branchdetail/BranchDetailScreen.kt` (new): `TabRow` + `HorizontalPager(pageCount = { 2 })`, page 0 = `CodeBrowserScreen`, page 1 = `HistoryScreen`, both passed through unmodified with their full existing parameter lists.
|
|
- `ui/navigation/Route.kt`: `CodeBrowser`/`History` replaced by `BranchDetail(owner, name, refKind, ref)`. `RouteStackSaver.kt`'s `RouteEncoding` updated to match (`TYPE_BRANCH_DETAIL` replaces `TYPE_CODE_BROWSER`/`TYPE_HISTORY`).
|
|
- `MainActivity.kt`: `onReferenceSelected` now maps the tapped reference's `ReferenceKind` (branch/tag) to `CodeReferenceKind` and pushes one `Route.BranchDetail`, matching what previously only fed `Route.CodeBrowser`'s `refKind` field — this mapping had been temporarily lost when TASK-024 redirected the tap straight to `Route.History` (which didn't need a ref kind); this task restores it.
|
|
- `RouteEncodingTest.kt` updated: `code browser route round-trips` and `history route round-trips` tests replaced by one `branch detail route round-trips including a ref containing a slash` test; the truncated-route and full-back-stack tests updated to match.
|
|
- `HistoryScreen.kt`'s `CommitDetailScreen`: each `BoxWithConstraints` branch now wraps `Header(...)` + content in `Column(Modifier.fillMaxSize()) { ... }` instead of leaving them as bare siblings.
|
|
- `BranchDetailScreen.kt`: outer `Column` gained `Modifier.statusBarsPadding()`.
|
|
- `HistoryScreen.kt`'s `DiffSideLine` (first pass, superseded — see below): `Modifier.horizontalScroll(horizontalScrollState).padding(...)` with `softWrap = false`, no `fillMaxWidth()`.
|
|
- `HistoryScreen.kt`'s `DiffSideLine`/`FileDiffView` (second pass, current): shared `ScrollState` replaced by a manually-driven offset — `rememberScrollableState { delta -> ... }` updates one `MutableFloatState` clamped to `[0, maxHorizontalOffsetPx]`, where `maxHorizontalOffsetPx` is computed once per selected file via `rememberTextMeasurer().measure(...)` against the longest line's character count (monospace, so any character stands in for the widest line's glyph width). Each row applies `Modifier.scrollable(sharedScrollableState, Orientation.Horizontal)` for gesture capture and `Modifier.offset { IntOffset(-horizontalOffsetPx.value.roundToInt(), 0) }` for rendering, wrapped in `Box(Modifier.fillMaxWidth().clipToBounds())` since a plain `offset` (unlike `horizontalScroll`) doesn't clip overflow on its own.
|
|
- `strings_repositories.xml`: `repositories_references_loading` simplified to a fixed "로딩 중" string (no `%1$s` placeholder); call site in `RepositoryListScreen.kt` updated to match.
|
|
- Back buttons removed from `CodeBrowserScreen.BrowserHeader`, `HistoryScreen.Header`, `NotificationsScreen.FilterTabs`, `WorkItemsScreen.ModeTabs`; `WorkItemsScreen.DetailHeader` composable and its 5 call sites removed entirely; `ReferencesContent`'s header back button removed. `history_back`/`notifications_back`/`workitems_back`/`codebrowser_back` string resources removed as now-unused.
|
|
- `RepositoryListScreen.kt`: `RepositoryContent`/`ReferencesContent` wrapped in `PullToRefreshBox` (`@OptIn(ExperimentalMaterial3Api::class)`), manual "새로고침" `OutlinedButton`s removed from both.
|
|
- `./gradlew compileDebugKotlin`: BUILD SUCCESSFUL, both after the `@OptIn(ExperimentalMaterial3Api::class)` fix for `PullToRefreshBox` and again after the diff-scroll rewrite.
|
|
- `./gradlew assembleDebug testDebugUnitTest lintDebug` (`--rerun-tasks` on the first full pass, incremental on the diff-scroll-rewrite follow-up): BUILD SUCCESSFUL both times, all unit tests passing, 0 lint errors.
|
|
- Live device: build installed over wireless `adb` (`adb-RFCYA0RD1CZ-ztOzAA._adb-tls-connect._tcp`, Galaxy Z Fold) three times across this task — initial batch, then again after the diff-scroll jitter report and fix; human product owner asked to confirm the diff view specifically no longer jitters, alongside the rest of the batch — result pending as of this commit.
|
|
|
|
## Review findings
|
|
|
|
None yet — pending Codex/human review.
|