Ubisam-MobileGit/tasks/027-image-and-csv-viewer.md
choiminyeong 2818dab09a TASK-027/028: image+CSV viewer with gallery swipe, numbered Issues/PR pagination
TASK-027: opening an image (PNG/JPEG/GIF/BMP/WebP, detected by magic bytes)
now decodes and shows it, with swipe navigation to the previous/next image
in the same folder; opening a .csv file shows a scrollable table instead of
raw text. Both reuse the tab-swipe/content-scroll gesture-conflict fix so
neither ever triggers the Files/History tab to change underneath it.

TASK-028: the Issues/PR list shows up to 5 numbered page buttons plus a
[ page / total ] jump field, backed by Gitea's X-Total-Count header
(verified against Gitea's own source) and a server-side type=issues filter
so the count excludes pull requests. Every other paginated list is
unchanged, per this session's earlier scoping decision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 21:50:24 +09:00

9.7 KiB

TASK-027: Image viewer (with swipe gallery navigation) and CSV table viewer in the code browser

Status: implemented; all required checks pass; live device verification pending (no image/CSV files were found in the repositories browsed live this session — see Known limitations) Owner: Claude Reviewer: Codex

Outcome

Two live requests, delivered together: opening an image file in the code browser now shows the image (not a generic "binary file" fallback), with left/right swipe to move to the previous/next image in the same folder; opening a .csv file shows a scrollable table (rows/columns, first row styled as a header) instead of raw comma-separated text. Both reuse the tab-swipe/content-scroll gesture-conflict fix from TASK-025's last follow-up: swiping either an image or a CSV table never triggers the Files/History tab to change underneath it.

Scope

  • Included: feature/codebrowser/content/ContentClassification.kt (new Image/Csv cases, new ImageFormat enum), feature/codebrowser/content/SourceContentClassifier.kt (magic-byte image detection, .csv-extension table parsing), feature/codebrowser/CodeBrowserViewModel.kt (siblingImagePaths on File state, navigateImage, type=issues-adjacent-but-unrelated — see TASK-028 for that; this task only touches the image gallery/path plumbing), feature/codebrowser/CodeBrowserScreen.kt (ImageContent, CsvTable/CsvRow/CsvCell), feature/branchdetail/BranchDetailScreen.kt (pager-gating condition widened to Image/Csv), MainActivity.kt (onNavigateImage wiring), domain/model/RepositoryModels.kt is untouched by this task (see TASK-028 for its Page.totalCount addition, done in the same session but a separate concern).
  • Excluded: No new image/CSV editing, no zoom/pan on images (only swipe-to-navigate was asked for), no header-row auto-detection for CSV (the first row is always styled as a header — see Design notes), no thumbnail/preview generation, no change to how non-image/non-CSV files render.

Design notes

  • Image detection is by magic bytes, not file extension (detectImageFormat checks the PNG/ JPEG/GIF/BMP/WebP signatures directly): a mislabeled or extensionless file still renders correctly if it genuinely is one of these formats, and a renamed non-image file isn't misclassified just because of its name. It runs before the existing NUL-byte-means-Binary check, since every one of these formats' bytes contain NULs and would otherwise always be caught as generic Binary first.
  • The sibling-image gallery is built from the directory listing already in hand, not a second fetch: CodeBrowserViewModel.openEntry captures the current Directory state's file entries (filtered to image extensions — extension-only, since building the gallery list can't afford to fetch and classify every sibling's bytes just to find out which are images) into the new File state's siblingImagePaths at the moment a file is opened. navigateImage(delta) then looks up the current path's index in that list and loads the neighbor, with no bound past either end. Opening a file outside a directory listing (currently only possible via retry() after an error) leaves siblingImagePaths empty — swipe navigation is simply unavailable there, a known, accepted edge case rather than extra plumbing through Loading/Error states for it.
  • The image swipe gesture is a discrete drag-distance threshold (detectHorizontalDragGestures
    • a 120px total-drag check on release), not a continuous scroll position — unlike the diff view's horizontal scroll, there's no partial-scroll state to render mid-drag, only "did the user swipe far enough to go to the next/previous image."
  • The CSV table reuses the diff view's manually-driven shared-offset pattern for its horizontal scroll (rememberScrollableState + Modifier.offset, one offset shared across all rows via Modifier.scrollable per row) rather than a plain shared Modifier.horizontalScroll. Every CSV cell here is the same fixed width, so the specific bound-thrashing bug that pattern was built to avoid (TASK-025's "글자가 춤을 추네") couldn't actually occur with uniform-width cells — but reusing the already-verified-correct pattern costs nothing and keeps the codebase's one way of doing synced horizontal scroll, rather than two.
  • No header-row detection for CSV: the first row is always rendered with a highlighted background, matching how most spreadsheet tools default a first-row-as-header assumption, since actually detecting whether a CSV has a header (vs. all-data) isn't reliably possible from the file alone.
  • Pager-swipe gating (from TASK-025's last follow-up) was widened, not duplicated: the same currentPageShowsScrollableCode check in BranchDetailScreen.kt now also covers ContentClassification.Image and ContentClassification.Csv, so the human product owner's explicit requirement — "탭 전환은 발생하면 안돼" (tab switching must not happen) while viewing either — is met by the existing mechanism rather than a new one.

Acceptance criteria

  • Opening a PNG/JPEG/GIF/BMP/WebP file (detected by magic bytes) shows the decoded image.
  • Swiping left/right on an image with siblings in the same folder moves to the next/previous image in that folder's listing order; swiping past either end does nothing.
  • Swiping (or any horizontal drag) on an image or a CSV table never changes the Files/History tab underneath it.
  • Opening a .csv file shows a scrollable table with the first row styled as a header; a non-.csv file with comma-separated content is unaffected (still plain text).
  • Quoted CSV fields containing commas and escaped ("") quotes parse correctly.
  • Build, unit tests, and lint pass.
  • Live device confirmation with a real image file and a real .csv file — not yet possible this session; see Known limitations.

Required checks

./gradlew assembleDebug
./gradlew testDebugUnitTest
./gradlew lintDebug

Evidence

  • ContentClassification.kt: new Image(sizeBytes, content: SourceContent, format: ImageFormat) and Csv(sizeBytes, rows: List<List<String>>, isTruncated: Boolean) cases; content reuses SourceContent's already-correct content-based equals()/hashCode() instead of holding a raw ByteArray (which would default to reference equality on a data class).
  • SourceContentClassifier.kt: classify(content, path: RepositoryPath? = null) — path is optional/defaulted so every pre-existing call site keeps compiling unchanged; detectImageFormat checks PNG/JPEG/GIF/BMP/WebP signatures before the NUL-byte Binary check; CSV parsing (renderCsv/parseCsv, a small RFC-4180-ish hand-written parser) only runs when path ends in .csv, reusing the same line/character truncation budgets as renderText.
  • CodeBrowserViewModel.kt: CodeBrowserUiState.File gained siblingImagePaths: List<RepositoryPath> = emptyList(); openEntry captures it from the current Directory state's file entries (via new private RepositoryPath.hasImageExtension()); new navigateImage(delta: Int).
  • CodeBrowserScreen.kt: new ImageContent (BitmapFactory decode + Compose Image + drag-threshold swipe), CsvTable/CsvRow/CsvCell (shared-offset horizontal scroll, matching the diff view's pattern); FileContent's signature grew path/siblingImagePaths/ onNavigateImage to support both.
  • BranchDetailScreen.kt: currentPageShowsScrollableCode's page-0 branch now also checks ContentClassification.Image/ContentClassification.Csv, not just Text.
  • New tests: SourceContentClassifierTest.kt (PNG/JPEG/GIF/BMP/WebP signature detection, an image signature's embedded NUL byte not short-circuiting to Binary, oversized-before-image-detection, .csv vs non-.csv path handling, no-path-given fallback to Text, quoted-field/escaped-quote parsing, row-count truncation); CodeBrowserViewModelTest.kt (gallery capture in listing order excluding a non-image sibling, navigateImage moving and stopping at both ends, navigateImage no-op when opened outside a directory listing).
  • ./gradlew compileDebugKotlin compileDebugUnitTestKotlin: BUILD SUCCESSFUL.
  • ./gradlew testDebugUnitTest: BUILD SUCCESSFUL, all unit tests passing (including the new ones above).
  • ./gradlew assembleDebug lintDebug: BUILD SUCCESSFUL, 0 lint errors.
  • Live device: build installed over wireless adb. Navigated several of the human product owner's actual repositories looking for an existing image or .csv file to confirm the rendering end-to-end; none were found among the repositories browsed this session (see Known limitations).

Known limitations

  • Not yet confirmed against a real image or CSV file on the live device. Every repository browsed this session for a suitable test file didn't have one readily reachable. The classification and rendering logic is covered thoroughly by unit tests instead (magic-byte detection, gallery navigation, CSV parsing), but a live human confirmation of the actual pixels on screen — decoded image quality, table cell alignment/scroll feel — is still outstanding.
  • The CSV table's column width is a fixed 120dp per cell, not sized to content — a column with very short values wastes horizontal space, and a column with very long values still clips at 120dp (readable via horizontal scroll, but not auto-fit). Not raised as a concern by the human product owner; flagged here in case it's worth revisiting once seen live.
  • navigateImage after a retry()-recovered error loses gallery position (see Design notes) — accepted, not treated as a defect.

Review findings

None yet — pending Codex/human review.