Skip to content

fix: restore @query annotations on InstalledAppDao monorepo lookups#483

Merged
rainxchzed merged 4 commits intomainfrom
feat/476-multi-platform-filter
May 2, 2026
Merged

fix: restore @query annotations on InstalledAppDao monorepo lookups#483
rainxchzed merged 4 commits intomainfrom
feat/476-multi-platform-filter

Conversation

@rainxchzed
Copy link
Copy Markdown
Member

@rainxchzed rainxchzed commented May 2, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Fixed potential null-related failures in broadcast intent handling.
  • New Features

    • Multi-platform selection: choose multiple platforms (Android, macOS, Windows, Linux) for discovery.
    • Platform filter UI: revamped popup with per-platform toggles and "Select all platforms" action.
    • Multi-topic filtering: supports selecting multiple topics for richer repository discovery.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 2, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ed3a1445-ae20-4ecb-89a7-e419dd09091d

📥 Commits

Reviewing files that changed from the base of the PR and between 686413a and 9914acf.

📒 Files selected for processing (1)
  • feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/repository/HomeRepositoryImpl.kt

Walkthrough

This PR converts home discovery from single-platform selection to multi-platform selection across domain, data, repository, and presentation layers; adds a DAO method to list installed apps by repo; and makes PackageEventReceiver's broadcast handling null-safe.

Changes

Multi-Platform Discovery Selection

Layer / File(s) Summary
Domain Model
core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/DiscoveryPlatform.kt
Adds selectablePlatforms list with Android, Macos, Windows, Linux.
Repository Interfaces
core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt
feature/home/domain/src/commonMain/kotlin/zed/rainxch/home/domain/repository/HomeRepository.kt
Replaces singular discovery platform API with plural set-based API: getDiscoveryPlatforms(): Flow<Set<DiscoveryPlatform>> / setDiscoveryPlatforms(...) and updates HomeRepository methods to accept platforms: Set<DiscoveryPlatform>.
Persistence & Data Layer
core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt
feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/repository/HomeRepositoryImpl.kt
TweaksRepositoryImpl persists discovery platforms as a string-set key with migration from legacy single key. HomeRepositoryImpl refactors fetch methods to accept Set<DiscoveryPlatform>, introduces loadCachedReposForSet(), normalizes platform sets, maps platforms to query topics, and updates caching keys to include platform-set tokens.
Presentation State & Actions
feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeState.kt
feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt
HomeState replaces single-selected topic/platform with selectedTopics: Set<TopicCategory> and selectedPlatforms: Set<DiscoveryPlatform> (empty = no filter). HomeAction adds OnSelectAllPlatforms and makes SwitchTopic accept a non-null TopicCategory.
Presentation Logic
feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt
ViewModel observes getDiscoveryPlatforms() (plural), maintains set-based selections, refactors loadRepos() and loadTopicSupplement() for multi-topic/platform flows, parallelizes cached topic fetches, and adds set-toggle helpers and actions (TogglePlatform, OnSelectAllPlatforms).
Presentation UI
feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt
HomeScreen/HomeTopAppBar and TopicChips updated to accept sets; platform popup hoisted to HomeScreen as PlatformsPopup with PlatformPopupRow, selectedPlatformsIcons() helper, and a window-anchored popup position provider.

Android Package Receiver & App DAO Support

Layer / File(s) Summary
Android Event Handling
core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt
Makes broadcast handling null-safe by using intent?.action for logging, when-dispatch, and error logs.
Data Access
core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/InstalledAppDao.kt
Adds suspending DAO method suspend fun getAppsByRepoId(repoId: Long): List<InstalledAppEntity> ordered by installedAt DESC.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as HomeRoot (UI)
    participant VM as HomeViewModel
    participant Repo as HomeRepository
    participant DB as Local Cache

    User->>UI: Toggle platform in popup
    UI->>VM: dispatch(TogglePlatform(platform))
    VM->>VM: Update selectedPlatforms set
    VM->>VM: Call loadRepos(platforms=targetPlatforms)
    VM->>Repo: getTrendingRepositories(platforms=targetPlatforms)
    Repo->>Repo: Normalize platforms → topics filter
    Repo->>DB: Check local cache (cacheKey: category, platforms, page)
    alt Cache miss
        Repo->>Repo: Fetch from GitHub with buildSimplifiedQuery(platforms)
        Repo->>DB: Write paginated results
    end
    DB-->>Repo: Cached/fresh repos
    Repo-->>VM: PaginatedDiscoveryRepositories
    VM->>VM: Update state.repositories
    VM-->>UI: Emit new HomeState
    UI->>UI: Re-render with selected platforms + filtered repos
    UI-->>User: Display results
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Hop through platforms far and wide,
Sets of choices by my side,
Topics multicolored, caches fast,
Snapshots fetched and filters cast,
Repos bloom now — select and glide.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title mentions restoring @query annotations on InstalledAppDao, but the actual changes introduce multi-platform filtering across home repository, discovery platform handling, and state management—none directly related to @query annotations. Update the title to reflect the primary change: multi-platform discovery filter refactoring, such as 'feat: support multi-platform filtering in home repository and discovery platforms'.
Docstring Coverage ⚠️ Warning Docstring coverage is 6.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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/476-multi-platform-filter

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: 2

🧹 Nitpick comments (5)
core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt (1)

107-111: ⚡ Quick win

scope.launch should be getBackstopScope().launch to survive receiver teardown on the manifest-registered path.

The local scope (CoroutineScope(SupervisorJob() + Dispatchers.IO), line 52) is tied to the receiver instance. Android may destroy the instance immediately after onReceive returns, cutting off any work launched here. getBackstopScope() exists precisely to hand off to the Koin app scope in that case — yet the two dispatch sites use scope directly, while every subsequent async operation (onPackageInstalled line 186, handleExternalInstall line 302, retry line 328) correctly uses getBackstopScope(). This leaves onPackageInstalled and onPackageRemoved silently cancellable at the point of entry.

♻️ Proposed fix
                Intent.ACTION_PACKAGE_ADDED,
                Intent.ACTION_PACKAGE_REPLACED,
                Intent.ACTION_MY_PACKAGE_REPLACED,
                -> {
-                    scope.launch { onPackageInstalled(packageName) }
+                    getBackstopScope().launch { onPackageInstalled(packageName) }
                }

                Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
-                    scope.launch { onPackageRemoved(packageName) }
+                    getBackstopScope().launch { onPackageRemoved(packageName) }
                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt`
around lines 107 - 111, The receiver uses a local CoroutineScope named scope to
launch work that can be cancelled when the BroadcastReceiver instance is torn
down; replace those launches with the app-level scope by calling
getBackstopScope().launch instead of scope.launch for the package events so work
survives receiver teardown — specifically change the two dispatch sites that
call scope.launch (the ACTION_PACKAGE_ADDED branch that launches
onPackageInstalled(packageName) and the ACTION_PACKAGE_FULLY_REMOVED branch that
launches onPackageRemoved(packageName)) to use getBackstopScope().launch so they
match other async paths (onPackageInstalled, handleExternalInstall, retry logic)
that already use getBackstopScope().
feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/repository/HomeRepositoryImpl.kt (1)

562-566: 💤 Low value

normalize() contains a redundant All-filter on line 564

Line 563 returns early whenever All is in the set, so by the time execution reaches line 564, All is guaranteed not to be in this. The filter { it != DiscoveryPlatform.All } is therefore always a no-op:

🔧 Proposed cleanup
 private fun Set<DiscoveryPlatform>.normalize(): Set<DiscoveryPlatform> {
     if (contains(DiscoveryPlatform.All)) return emptySet()
-    val real = filter { it != DiscoveryPlatform.All }.toSet()
+    val real = this  // `All` already handled above
     return if (real.size == DiscoveryPlatform.selectablePlatforms.size) emptySet() else real
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/repository/HomeRepositoryImpl.kt`
around lines 562 - 566, The normalize() extension on Set<DiscoveryPlatform>
redundantly filters out DiscoveryPlatform.All after already returning early when
All is present; remove the unnecessary filter and simply use the current set
(e.g., val real = this.toSet() or val real = this) when computing real, then
keep the existing size comparison against DiscoveryPlatform.selectablePlatforms
to decide whether to return emptySet() or real; update the body of normalize()
accordingly to eliminate the no-op filter of DiscoveryPlatform.All.
feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt (2)

320-340: 💤 Low value

Sequential live search per topic may skip remaining topics on failure.

If homeRepository.searchByTopic(...).collect throws for one topic, the exception propagates to the outer catch block, skipping subsequent topics in the forEach. Consider wrapping individual topic searches to isolate failures:

♻️ Isolate per-topic failures
                     topics.forEach { topic ->
+                        try {
                         homeRepository
                             .searchByTopic(
                                 searchKeywords = topic.searchKeywords,
                                 platforms = platforms,
                                 page = 1,
                             ).collect { paginatedRepos ->
                                 val newReposWithStatus = mapReposToUi(paginatedRepos.repos)

                                 _state.update { currentState ->
                                     val merged =
                                         (currentState.repos + newReposWithStatus)
                                             .distinctBy { it.repository.fullName }

                                     currentState.copy(
                                         repos = merged.toImmutableList(),
                                         hasMorePages = currentState.hasMorePages || paginatedRepos.hasMore,
                                     )
                                 }
                             }
+                        } catch (t: Throwable) {
+                            if (t is CancellationException) throw t
+                            logger.warn("Topic supplement search for $topic failed: ${t.message}")
+                        }
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt`
around lines 320 - 340, The forEach over topics in HomeViewModel.kt currently
calls homeRepository.searchByTopic(...).collect which, if it throws for one
topic, aborts the loop and skips remaining topics; wrap each per-topic
search/collect in its own try-catch (or use runCatching) around the call to
homeRepository.searchByTopic and the mapping via mapReposToUi so that any
exception is caught, logged (or handled) and the loop continues, while
preserving the existing _state.update behavior for successful results.

162-172: 💤 Low value

Async deferred evaluated unconditionally when platforms is null.

The targetPlatformsDeferred is always launched but only awaited when platforms == null. When platforms is provided, the async job still runs to completion in the background unnecessarily.

Consider making the deferred lazy or only launching when needed:

♻️ Suggested improvement
-        val targetPlatformsDeferred =
-            viewModelScope.async {
-                tweaksRepository.getDiscoveryPlatforms().first()
-            }
         val targetTopics = if (topicsExplicitlySet) topics.orEmpty() else _state.value.selectedTopics

         logger.debug("Loading repos: category=$targetCategory, topics=$targetTopics, page=$nextPageIndex, isInitial=$isInitial")

         return viewModelScope
             .launch {
-                val targetPlatforms = platforms ?: targetPlatformsDeferred.await()
+                val targetPlatforms = platforms ?: tweaksRepository.getDiscoveryPlatforms().first()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt`
around lines 162 - 172, In HomeViewModel, avoid launching the background async
job unconditionally: the targetPlatformsDeferred created via
viewModelScope.async is started even when platforms is supplied; either create
the deferred with viewModelScope.async(start = CoroutineStart.LAZY) and only
call await() when platforms == null, or move the viewModelScope.async call
inside the branch where platforms is null so it only starts when needed; update
references to targetPlatformsDeferred.await() used in the launch block (and keep
logger/variable names: targetPlatformsDeferred, platforms, targetPlatforms) so
the deferred is only started/awaited when required.
feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt (1)

744-758: 💤 Low value

Selection indicator causes layout shift.

When isSelected toggles, the Done icon conditionally renders, causing the text to shift horizontally. Consider using a fixed-width container or Modifier.alpha(0f) for the unselected state to maintain stable layout:

♻️ Stable layout with invisible placeholder
         Row(
             verticalAlignment = Alignment.CenterVertically,
             horizontalArrangement =
                 Arrangement.spacedBy(
                     6.dp,
                     Alignment.Start,
                 ),
         ) {
-            if (isSelected) {
-                Icon(
-                    imageVector = Icons.Default.Done,
-                    contentDescription = null,
-                    tint = MaterialTheme.colorScheme.primary,
-                    modifier = Modifier.size(20.dp),
-                )
-            }
+            Icon(
+                imageVector = Icons.Default.Done,
+                contentDescription = null,
+                tint = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent,
+                modifier = Modifier.size(20.dp),
+            )

             Text(
                 text = label,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt`
around lines 744 - 758, The conditional Icon rendering makes the Text shift when
isSelected toggles; always reserve the icon space instead: render the Icon
unconditionally (the same Icon call in HomeRoot.kt) but set its visibility via
Modifier.alpha(if (isSelected) 1f else 0f) or wrap it in a fixed-width
container/Spacer matching Modifier.size(20.dp) so the Text (the Text(...) call)
keeps a stable position when isSelected changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt`:
- Around line 152-165: The legacy-branch in getDiscoveryPlatforms() reads
DISCOVERY_PLATFORM_KEY but never writes DISCOVERY_PLATFORMS_KEY, so migration
never completes; modify the branch so that when stored == null and a legacy
non-All value is found, perform a one-shot write-through to backfill
DISCOVERY_PLATFORMS_KEY (using preferences.edit to set the new key to a set
containing the migrated platform) and then return that set; ensure the write
only happens when migrating (stored == null && legacy != All) to avoid loops and
leave the rest of the mapping behavior unchanged (references:
getDiscoveryPlatforms(), DISCOVERY_PLATFORMS_KEY, DISCOVERY_PLATFORM_KEY,
setDiscoveryPlatforms).

In
`@feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/repository/HomeRepositoryImpl.kt`:
- Around line 545-549: The current query builder in HomeRepositoryImpl (the when
block that inspects topics and uses baseQuery and topics.joinToString)
incorrectly groups multiple topic qualifiers with parentheses, which GitHub REST
search treats as literal text; change the multi-topic branch to repeat the full
baseQuery for each topic and join those full queries with " OR " (e.g.,
topics.joinToString(" OR ") { "$baseQuery topic:$it" }) so each topic becomes
its own complete query clause instead of a parenthesized group.

---

Nitpick comments:
In
`@core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt`:
- Around line 107-111: The receiver uses a local CoroutineScope named scope to
launch work that can be cancelled when the BroadcastReceiver instance is torn
down; replace those launches with the app-level scope by calling
getBackstopScope().launch instead of scope.launch for the package events so work
survives receiver teardown — specifically change the two dispatch sites that
call scope.launch (the ACTION_PACKAGE_ADDED branch that launches
onPackageInstalled(packageName) and the ACTION_PACKAGE_FULLY_REMOVED branch that
launches onPackageRemoved(packageName)) to use getBackstopScope().launch so they
match other async paths (onPackageInstalled, handleExternalInstall, retry logic)
that already use getBackstopScope().

In
`@feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/repository/HomeRepositoryImpl.kt`:
- Around line 562-566: The normalize() extension on Set<DiscoveryPlatform>
redundantly filters out DiscoveryPlatform.All after already returning early when
All is present; remove the unnecessary filter and simply use the current set
(e.g., val real = this.toSet() or val real = this) when computing real, then
keep the existing size comparison against DiscoveryPlatform.selectablePlatforms
to decide whether to return emptySet() or real; update the body of normalize()
accordingly to eliminate the no-op filter of DiscoveryPlatform.All.

In
`@feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt`:
- Around line 744-758: The conditional Icon rendering makes the Text shift when
isSelected toggles; always reserve the icon space instead: render the Icon
unconditionally (the same Icon call in HomeRoot.kt) but set its visibility via
Modifier.alpha(if (isSelected) 1f else 0f) or wrap it in a fixed-width
container/Spacer matching Modifier.size(20.dp) so the Text (the Text(...) call)
keeps a stable position when isSelected changes.

In
`@feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt`:
- Around line 320-340: The forEach over topics in HomeViewModel.kt currently
calls homeRepository.searchByTopic(...).collect which, if it throws for one
topic, aborts the loop and skips remaining topics; wrap each per-topic
search/collect in its own try-catch (or use runCatching) around the call to
homeRepository.searchByTopic and the mapping via mapReposToUi so that any
exception is caught, logged (or handled) and the loop continues, while
preserving the existing _state.update behavior for successful results.
- Around line 162-172: In HomeViewModel, avoid launching the background async
job unconditionally: the targetPlatformsDeferred created via
viewModelScope.async is started even when platforms is supplied; either create
the deferred with viewModelScope.async(start = CoroutineStart.LAZY) and only
call await() when platforms == null, or move the viewModelScope.async call
inside the branch where platforms is null so it only starts when needed; update
references to targetPlatformsDeferred.await() used in the launch block (and keep
logger/variable names: targetPlatformsDeferred, platforms, targetPlatforms) so
the deferred is only started/awaited when required.
🪄 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: aa687377-e1c4-4505-a39a-fcbc3c96c443

📥 Commits

Reviewing files that changed from the base of the PR and between 1cb9453 and 686413a.

⛔ Files ignored due to path filters (1)
  • feature/home/.DS_Store is excluded by !**/.DS_Store
📒 Files selected for processing (11)
  • core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/InstalledAppDao.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/DiscoveryPlatform.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt
  • feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/repository/HomeRepositoryImpl.kt
  • feature/home/domain/src/commonMain/kotlin/zed/rainxch/home/domain/repository/HomeRepository.kt
  • feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt
  • feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt
  • feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeState.kt
  • feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt

Comment on lines +152 to 165
override fun getDiscoveryPlatforms(): Flow<Set<DiscoveryPlatform>> =
preferences.data.map { prefs ->
val platform = prefs[DISCOVERY_PLATFORM_KEY]
DiscoveryPlatform.fromName(platform)
val stored = prefs[DISCOVERY_PLATFORMS_KEY]
if (stored != null) {
stored
.mapNotNull { name ->
DiscoveryPlatform.entries.find { it.name == name && it != DiscoveryPlatform.All }
}.toSet()
} else {
// Legacy single-platform key migration: map old `All` to empty set.
val legacy = prefs[DISCOVERY_PLATFORM_KEY]?.let { DiscoveryPlatform.fromName(it) }
if (legacy != null && legacy != DiscoveryPlatform.All) setOf(legacy) else emptySet()
}
}
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Migration never graduates to the new key until the user explicitly saves

The legacy fallback branch (lines 160–163) reads DISCOVERY_PLATFORM_KEY on every emission but never back-fills DISCOVERY_PLATFORMS_KEY. For existing users whose old key is set, every cold-start read of getDiscoveryPlatforms() will continue hitting the legacy path indefinitely — until setDiscoveryPlatforms is explicitly called (e.g. when the user changes their platform selection).

Consider performing a one-shot write-through when migrating, so subsequent reads go straight to the new key:

💡 Proposed fix — write-through on first migration
 } else {
     // Legacy single-platform key migration: map old `All` to empty set.
     val legacy = prefs[DISCOVERY_PLATFORM_KEY]?.let { DiscoveryPlatform.fromName(it) }
-    if (legacy != null && legacy != DiscoveryPlatform.All) setOf(legacy) else emptySet()
+    val migrated = if (legacy != null && legacy != DiscoveryPlatform.All) setOf(legacy) else emptySet()
+    // Back-fill so future reads use the new key directly.
+    // Note: DataStore.data.map is read-only; perform the write in a side-channel
+    // or remove this comment if you intentionally prefer lazy migration.
+    migrated
 }

If you intentionally prefer the lazy approach, a short comment explaining that setDiscoveryPlatforms is always called before the legacy key matters in practice would be sufficient.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override fun getDiscoveryPlatforms(): Flow<Set<DiscoveryPlatform>> =
preferences.data.map { prefs ->
val platform = prefs[DISCOVERY_PLATFORM_KEY]
DiscoveryPlatform.fromName(platform)
val stored = prefs[DISCOVERY_PLATFORMS_KEY]
if (stored != null) {
stored
.mapNotNull { name ->
DiscoveryPlatform.entries.find { it.name == name && it != DiscoveryPlatform.All }
}.toSet()
} else {
// Legacy single-platform key migration: map old `All` to empty set.
val legacy = prefs[DISCOVERY_PLATFORM_KEY]?.let { DiscoveryPlatform.fromName(it) }
if (legacy != null && legacy != DiscoveryPlatform.All) setOf(legacy) else emptySet()
}
}
override fun getDiscoveryPlatforms(): Flow<Set<DiscoveryPlatform>> =
preferences.data.map { prefs ->
val stored = prefs[DISCOVERY_PLATFORMS_KEY]
if (stored != null) {
stored
.mapNotNull { name ->
DiscoveryPlatform.entries.find { it.name == name && it != DiscoveryPlatform.All }
}.toSet()
} else {
// Legacy single-platform key migration: map old `All` to empty set.
val legacy = prefs[DISCOVERY_PLATFORM_KEY]?.let { DiscoveryPlatform.fromName(it) }
val migrated = if (legacy != null && legacy != DiscoveryPlatform.All) setOf(legacy) else emptySet()
// Back-fill so future reads use the new key directly.
// Note: DataStore.data.map is read-only; perform the write in a side-channel
// or remove this comment if you intentionally prefer lazy migration.
migrated
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt`
around lines 152 - 165, The legacy-branch in getDiscoveryPlatforms() reads
DISCOVERY_PLATFORM_KEY but never writes DISCOVERY_PLATFORMS_KEY, so migration
never completes; modify the branch so that when stored == null and a legacy
non-All value is found, perform a one-shot write-through to backfill
DISCOVERY_PLATFORMS_KEY (using preferences.edit to set the new key to a set
containing the migrated platform) and then return that set; ensure the write
only happens when migrating (stored == null && legacy != All) to avoid loops and
leave the rest of the mapping behavior unchanged (references:
getDiscoveryPlatforms(), DISCOVERY_PLATFORMS_KEY, DISCOVERY_PLATFORM_KEY,
setDiscoveryPlatforms).

…repository/HomeRepositoryImpl.kt

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@rainxchzed rainxchzed merged commit fc421d0 into main May 2, 2026
1 check was pending
@rainxchzed rainxchzed deleted the feat/476-multi-platform-filter branch May 2, 2026 10:44
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