Skip to content

Add from starred picker (#444 ask 2/3)#503

Merged
rainxchzed merged 3 commits into
mainfrom
feat/444-add-from-starred
May 4, 2026
Merged

Add from starred picker (#444 ask 2/3)#503
rainxchzed merged 3 commits into
mainfrom
feat/444-add-from-starred

Conversation

@rainxchzed
Copy link
Copy Markdown
Member

@rainxchzed rainxchzed commented May 4, 2026

Second of three PRs for #444. Surfaces APK-shipping repos from the user's GitHub stars and routes them into the standard install flow. See plan in `roadmap/444_B_STARRED_REPOS_SYNC.md`.

What's in here

  • New Add from starred entry in the Apps screen overflow menu.
  • New `StarredPickerScreen` route. ViewModel:
    1. Loads starred from local cache (already synced by the existing starred feature)
    2. Scans each starred repo's latest release for `.apk` / `.apks` / `.xapk` / `.aab` assets, batched with progress
    3. Cross-checks against `InstalledAppsRepository` for already-tracked dedup (matched by `owner/name`, case-insensitive)
  • Header shows transparent counts: "X starred · Y ship APKs · Z already tracked".
  • Search + sort (Recently starred / A→Z / Most stars) + "show repos without APK releases" toggle.
  • Per-row tap routes to the existing Details screen, where the user installs through the normal flow — single-tap-to-track is intentionally not shipped here (see "What's deferred" below).
  • Rate-limit-safe: 429 mid-scan freezes the picker and exposes a Resume button. Partial results survive.
  • A11y: each row's content description includes "owner/repo, ships APK, already tracked, latest tag".
  • One-time tooltip: "Removing a star on GitHub won't untrack the app." Stars stay social signals; tracked apps stay intentional commitments.
  • What's-new bullet shipped in 16.json across 13 locales.

What's deferred

Plan B originally proposed bulk one-tap "Add 30 apps". The `InstalledApp` Room schema requires `packageName` as the primary key, and starred-repo metadata doesn't carry a package name (the repo's APK might pin a different applicationId, the user might not have it installed yet, etc.). True bulk-add without an installed APK requires a tracking-only schema with synthetic-key support — bigger change than this PR's scope.

This phase 1 ships the discovery surface today: scan starred for APKs, surface them, route to Details for normal install/track. Phase 2 (true bulk-track without local install) is a follow-up tracked separately once the schema can support it.

The UX is honest about the constraint — per-row tap goes through the existing flow, not a fake one-tap-add. No silent failures, no half-tracked rows.

Test plan

  • Signed-in user with starred repos sees the picker populated within 5s on first open.
  • Per-repo APK-scan progress is visible and accurate.
  • Repo with `.apk` asset → APK badge shown. Repo without → hidden by default; toggle reveals it.
  • Already-tracked repo → "Tracked" badge. Tapping still navigates to Details (so the user can update / inspect).
  • 429 mid-scan → Resume button appears, partial list still tappable.
  • Signed-out user → empty state with "Sign in to GitHub..." message.
  • User with 0 starred repos → empty state with "No starred repos..." message.
  • TalkBack reads each row's full content description.

Summary by CodeRabbit

  • New Features

    • "Add from starred" — browse your GitHub‑starred repos that ship APKs and jump to install.
    • New Starred picker UI — searchable, sortable (recent/alphabetical/most stars), filter to hide/show APK-less, scan progress, APK/tracked badges, rate‑limit resume, and navigate to repo details.
    • Apps list: top‑bar action to open the Starred picker.
  • Documentation / Localization

    • Updated "What's New" release notes in multiple languages to mention Add from starred (and related items).

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Adds a "starred picker" feature: new UI, state, actions/events, ViewModel, navigation destination and wiring, DI registration, localized strings, and release-note updates to surface APK-shipping repos from a user’s GitHub stars and navigate into details/install flow.

Changes

Starred Picker Feature

Layer / File(s) Summary
State & Action Contracts
feature/apps/presentation/.../starred/StarredPickerState.kt, .../StarredPickerAction.kt, .../StarredPickerEvent.kt
Adds StarredPickerState, StarredCandidateUi, StarredPickerSortRule, StarredPickerAction variants, and StarredPickerEvent navigation events.
Core Logic / ViewModel
feature/apps/presentation/.../starred/StarredPickerViewModel.kt
Implements StarredPickerViewModel: bootstraps starred sync, builds candidate list, scans releases for APK-like assets, handles search/sort/filter/toggle, manages rate-limit and resume scanning, and emits navigation events.
UI / Presentational Components
feature/apps/presentation/.../starred/StarredPickerRoot.kt, .../components/StarredCandidateRow.kt
Adds StarredPickerRoot composable (Scaffold, TopAppBar, search, sort chips, APK filter, scanning progress, rate-limit/resume UI, candidate list) and StarredCandidateRow (avatar, metadata, badges, star formatting, accessibility labels).
Integration: Navigation & DI
composeApp/.../navigation/GithubStoreGraph.kt, .../AppNavigation.kt, .../di/ViewModelsModule.kt
Adds GithubStoreGraph.StarredPickerScreen destination, composes StarredPickerRoot route with back/details callbacks, wires AppsRoot navigation callback, and registers StarredPickerViewModel in Koin via viewModelOf(::StarredPickerViewModel).
Apps Feature Integration
feature/apps/presentation/AppsRoot.kt, AppsAction.kt, AppsViewModel.kt
Adds AppsAction.OnAddFromStarredClick, extends AppsRoot signature with onNavigateToStarredPicker, adds overflow menu item (Star icon) to dispatch the action, and adds empty handler branch in AppsViewModel.onAction.
Localization & Release Notes
core/presentation/.../values/strings.xml, core/presentation/.../files/whatsnew/*/16.json
Adds ~18 string resources for the picker UI and inserts the "Add from starred" bullet into localized "What's New" JSON files across multiple locales.

Sequence Diagram

sequenceDiagram
    participant User
    participant AppsScreen
    participant Navigation
    participant StarredPickerUI as StarredPickerRoot
    participant VM as StarredPickerViewModel
    participant GitHub
    participant LocalRepo as InstalledAppsRepository/LocalDB

    User->>AppsScreen: Tap "Add from starred"
    AppsScreen->>Navigation: navigate(StarredPickerScreen)
    Navigation->>StarredPickerUI: render
    StarredPickerUI->>VM: collect state / observe events
    VM->>LocalRepo: check auth + load installed apps
    alt not authenticated
        VM-->>StarredPickerUI: phase = Empty
        StarredPickerUI->>User: show "sign in required"
    else authenticated
        VM->>GitHub: sync starred repos / load cached stars
        VM->>LocalRepo: build tracked keys
        VM-->>StarredPickerUI: candidates, phase = ScanningReleases
        loop per candidate
            VM->>GitHub: fetch latest release
            GitHub-->>VM: release assets
            VM-->>StarredPickerUI: update candidate.hasApkRelease / progress
        end
        VM-->>StarredPickerUI: phase = Ready
        User->>StarredPickerUI: click candidate
        StarredPickerUI->>VM: OnCandidateClick
        VM-->>Navigation: NavigateToDetails(repoId, owner, repo)
        Navigation->>User: navigate to DetailsScreen
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰
I nibble through stars at the pale moonlight,
I sniff for APKs and sort them by sight,
With chips and search I show what to track,
Tap a row, hop—into install we crack!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add from starred picker (#444 ask 2/3)' clearly and concisely summarizes the main feature being added—a picker interface for discovering and adding apps from GitHub starred repositories. It directly reflects the core functionality introduced across the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/444-add-from-starred

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt (1)

240-241: 💤 Low value

Unreachable branch — OnAddFromStarredClick is fully handled in AppsRoot before reaching the ViewModel.

AppsRoot.onAction intercepts AppsAction.OnAddFromStarredClick in its own when block (calling onNavigateToStarredPicker()) and only the else branch forwards to viewModel.onAction(). This mirrors the pre-existing empty OnNavigateBackClick -> {} at line 237.

If this is intentional for exhaustiveness/documentation purposes, consider a comment clarifying that; otherwise the branch can be removed since the sealed when here is a statement (not an expression) and doesn't require it.

♻️ Optional: remove the unreachable branch
-            AppsAction.OnAddFromStarredClick -> {
-            }
-
             is AppsAction.OnSearchChange -> {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt`
around lines 240 - 241, The empty AppsAction.OnAddFromStarredClick branch in
AppsViewModel is unreachable because AppsRoot.onAction intercepts that action
(calling onNavigateToStarredPicker()) before forwarding other actions to
viewModel.onAction; remove the AppsAction.OnAddFromStarredClick -> { } case from
AppsViewModel.onAction to simplify the sealed when, or if you want to keep it
for documentation/exhaustiveness add a one-line comment referencing
AppsRoot.onAction handling and mark it intentionally unreachable; update the
AppsViewModel.onAction method accordingly to avoid dead branches.
feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerViewModel.kt (1)

1-2: 💤 Low value

Remove unused @OptIn(ExperimentalTime::class) annotation.

The ExperimentalTime opt-in is declared but no experimental time APIs are used in this file. This appears to be leftover from a previous iteration.

♻️ Suggested cleanup
-@file:OptIn(ExperimentalTime::class)
-
 package zed.rainxch.apps.presentation.starred

Also remove the unused import:

-import kotlin.time.ExperimentalTime
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerViewModel.kt`
around lines 1 - 2, Remove the unused opt-in and related import: delete the
`@OptIn`(ExperimentalTime::class) annotation at the top of
StarredPickerViewModel.kt and also remove the unused import for
kotlin.time.ExperimentalTime (or any kotlin.time.* import) so the file no longer
declares an unnecessary ExperimentalTime opt-in.
feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerRoot.kt (1)

69-69: ⚡ Quick win

Consider using collectAsStateWithLifecycle() for lifecycle-aware collection.

Other screens in this codebase (e.g., AppNavigation.kt lines 70, 74) use collectAsStateWithLifecycle() from androidx.lifecycle.compose to pause collection when the app is in the background. Using plain collectAsState() may cause unnecessary work when the composable isn't visible.

♻️ Suggested change
-import androidx.compose.runtime.collectAsState
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
-    val state by viewModel.state.collectAsState()
+    val state by viewModel.state.collectAsStateWithLifecycle()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerRoot.kt`
at line 69, Replace the direct StateFlow collection call using
viewModel.state.collectAsState() in StarredPickerRoot.kt with the
lifecycle-aware collectAsStateWithLifecycle() to pause collection when the
composable is not visible: update the expression val state by
viewModel.state.collectAsState() to use collectAsStateWithLifecycle(), add the
necessary import from androidx.lifecycle.compose.collectAsStateWithLifecycle,
and ensure the module has the lifecycle-compose dependency so collection is
lifecycle-aware.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/components/StarredCandidateRow.kt`:
- Line 92: Replace the Spacer usage in StarredCandidateRow (the
Spacer(Modifier.padding(top = 2.dp)) instance) so it uses a sizing modifier
instead of padding — change it to use Modifier.height(2.dp) to produce the
intended 2dp vertical gap; ensure the Spacer import and surrounding composable
remain unchanged.
- Around line 44-51: The accessibility label in StarredCandidateRow (a11yLabel)
uses hardcoded English fragments; replace those literals with localized string
resources by adding keys like starred_picker_cd_ships_apk,
starred_picker_cd_already_tracked, and starred_picker_cd_latest (with a
placeholder for the tag) in strings.xml for all locales, then call
stringResource(...) inside the `@Composable` to append the localized texts when
candidate.hasApkRelease, candidate.isAlreadyTracked, and
candidate.latestReleaseTag != null (use the placeholder-based resource to inject
latestReleaseTag).
- Around line 141-153: The Badge composable applies clip but no background fill
so the pill isn't visible; update the Row modifier in Badge to include a
background using the same RoundedCornerShape and a translucent variant of the
passed color (e.g., color.copy(alpha = 0.15f)) before or after clip, so the
badge has a filled pill appearance, and add the required import for
androidx.compose.foundation.background; locate the Badge function to change its
Modifier chain and keep Icon/Text tinting as-is.

In
`@feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerRoot.kt`:
- Around line 97-101: The Icon used in StarredPickerRoot.kt sets
contentDescription = null which prevents screen readers from announcing the back
button; replace the null with a localized string via stringResource (e.g.,
contentDescription = stringResource(R.string.back_button_description) or
R.string.back) and add the required import for
androidx.compose.ui.res.stringResource and the new string resource entry
(back_button_description) in your resources so the navigation icon is accessible
to assistive technologies.

In
`@feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerViewModel.kt`:
- Around line 187-191: The catch block that handles Exception when updating
_state (inside the scan loop updating scanProgress via current.copy(scanProgress
= ++processed)) is swallowing errors; modify the catch to record or log the
exception (use the project's logger if available, or add the exception message
to state—e.g., a debug or error field or increment a failure counter on the
ViewModel state) while still incrementing processed so behavior is unchanged;
update the catch around the code handling processed and _state.update to include
the exception information (e.g., call logger.error(...) or update
current.copy(failedCount = current.failedCount + 1, lastError = e.message)) so
failures are preserved for debugging.

---

Nitpick comments:
In
`@feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt`:
- Around line 240-241: The empty AppsAction.OnAddFromStarredClick branch in
AppsViewModel is unreachable because AppsRoot.onAction intercepts that action
(calling onNavigateToStarredPicker()) before forwarding other actions to
viewModel.onAction; remove the AppsAction.OnAddFromStarredClick -> { } case from
AppsViewModel.onAction to simplify the sealed when, or if you want to keep it
for documentation/exhaustiveness add a one-line comment referencing
AppsRoot.onAction handling and mark it intentionally unreachable; update the
AppsViewModel.onAction method accordingly to avoid dead branches.

In
`@feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerRoot.kt`:
- Line 69: Replace the direct StateFlow collection call using
viewModel.state.collectAsState() in StarredPickerRoot.kt with the
lifecycle-aware collectAsStateWithLifecycle() to pause collection when the
composable is not visible: update the expression val state by
viewModel.state.collectAsState() to use collectAsStateWithLifecycle(), add the
necessary import from androidx.lifecycle.compose.collectAsStateWithLifecycle,
and ensure the module has the lifecycle-compose dependency so collection is
lifecycle-aware.

In
`@feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerViewModel.kt`:
- Around line 1-2: Remove the unused opt-in and related import: delete the
`@OptIn`(ExperimentalTime::class) annotation at the top of
StarredPickerViewModel.kt and also remove the unused import for
kotlin.time.ExperimentalTime (or any kotlin.time.* import) so the file no longer
declares an unnecessary ExperimentalTime opt-in.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ae7099b0-c8bf-443b-8a35-526397b8d0c3

📥 Commits

Reviewing files that changed from the base of the PR and between 3c477cc and b489b2e.

📒 Files selected for processing (26)
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt
  • core/presentation/src/commonMain/composeResources/files/whatsnew/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ar/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/bn/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/es/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/fr/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/hi/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/it/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ja/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ko/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/pl/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ru/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/tr/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/zh-CN/16.json
  • core/presentation/src/commonMain/composeResources/values/strings.xml
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerAction.kt
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerEvent.kt
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerRoot.kt
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerState.kt
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerViewModel.kt
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/components/StarredCandidateRow.kt

Comment on lines +44 to +51
val a11yLabel = buildString {
append(candidate.owner)
append(" / ")
append(candidate.name)
if (candidate.hasApkRelease) append(", ships APK")
if (candidate.isAlreadyTracked) append(", already tracked")
candidate.latestReleaseTag?.let { append(", latest ").append(it) }
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Accessibility label contains hardcoded English strings — localize them.

The strings ", ships APK", ", already tracked", and ", latest " are baked in as English literals. Since this is a @Composable function, stringResource is callable here. These should use localized resources so TalkBack announces the correct language for all 13 supported locales.

♻️ Proposed fix

Add dedicated string resources (e.g., starred_picker_cd_ships_apk, starred_picker_cd_already_tracked, starred_picker_cd_latest) to strings.xml and all locale files, then:

+    val shipsApkLabel  = stringResource(Res.string.starred_picker_cd_ships_apk)
+    val trackedLabel   = stringResource(Res.string.starred_picker_cd_already_tracked)
+    val latestLabel    = stringResource(Res.string.starred_picker_cd_latest)
     val a11yLabel = buildString {
         append(candidate.owner)
         append(" / ")
         append(candidate.name)
-        if (candidate.hasApkRelease) append(", ships APK")
-        if (candidate.isAlreadyTracked) append(", already tracked")
-        candidate.latestReleaseTag?.let { append(", latest ").append(it) }
+        if (candidate.hasApkRelease) append(", $shipsApkLabel")
+        if (candidate.isAlreadyTracked) append(", $trackedLabel")
+        candidate.latestReleaseTag?.let { append(", $latestLabel ").append(it) }
     }

As per coding guidelines, "Localize all user-facing strings using the 13-language localization system provided by core/presentation."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/components/StarredCandidateRow.kt`
around lines 44 - 51, The accessibility label in StarredCandidateRow (a11yLabel)
uses hardcoded English fragments; replace those literals with localized string
resources by adding keys like starred_picker_cd_ships_apk,
starred_picker_cd_already_tracked, and starred_picker_cd_latest (with a
placeholder for the tag) in strings.xml for all locales, then call
stringResource(...) inside the `@Composable` to append the localized texts when
candidate.hasApkRelease, candidate.isAlreadyTracked, and
candidate.latestReleaseTag != null (use the placeholder-based resource to inject
latestReleaseTag).

@rainxchzed rainxchzed force-pushed the feat/444-add-from-starred branch from 244ccb0 to 6d8e199 Compare May 4, 2026 11:37
@rainxchzed rainxchzed merged commit c7597f1 into main May 4, 2026
1 check was pending
@rainxchzed rainxchzed deleted the feat/444-add-from-starred branch May 4, 2026 11:39
@coderabbitai coderabbitai Bot mentioned this pull request May 9, 2026
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant