feat(library): add cross-store awareness and source switcher - #1760
feat(library): add cross-store awareness and source switcher#1760Meloon33 wants to merge 2 commits into
Conversation
📝 WalkthroughWalkthroughAdds cross-store game matching and metadata, alternate-source icons and installation indicators, plus detail-screen navigation and messaging for games available or installed through other stores. ChangesCross-store game awareness
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant LibraryViewModel
participant AppScreenContent
participant LibraryScreen
participant AppInfoList
LibraryViewModel->>AppScreenContent: provide sibling sources and install state
AppScreenContent->>LibraryScreen: call onSourceClick(source)
LibraryScreen->>AppInfoList: find normalized-name/source match
AppInfoList-->>LibraryScreen: return matching entry
LibraryScreen-->>AppScreenContent: update selected app and detail content
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
app/src/main/java/app/gamenative/ui/screen/library/components/LibraryGridCard.kt (1)
516-537: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse an “installed elsewhere” accessibility label.
When only
isInstalledOnOtherSourceis true, the check icon still exposeslibrary_installed, telling assistive technologies that the current store’s version is installed locally. Uselibrary_installed_elsewherewhen!isInstalled.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/app/gamenative/ui/screen/library/components/LibraryGridCard.kt` around lines 516 - 537, Update the check Icon inside GridStatusIcons so its contentDescription uses library_installed_elsewhere when !isInstalled, while retaining library_installed for locally installed items. Keep the existing icon tint and rendering behavior unchanged.app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListCard.kt (1)
246-254: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winMake installed-elsewhere reachable for non-Steam entries.
!isSteammatches every GOG, Epic, Amazon, and custom entry before Line 253, so those cards always show “Ready.” Because non-Steam entries also initializeisInstalled = trueat Lines 224-230, merely moving the new branch belowisInstalledwould still fail. Preserve Steam-local precedence, then check installed-elsewhere before the generic non-Steam fallback.Proposed precedence
when { - !isSteam -> stringResource(R.string.library_status_ready) to MaterialTheme.colorScheme.tertiary isDownloading -> "${(downloadProgress * 100).toInt()}%" to MaterialTheme.colorScheme.primary - isInstalled -> stringResource(R.string.library_installed) to MaterialTheme.colorScheme.tertiary + isInstalled && isSteam -> stringResource(R.string.library_installed) to MaterialTheme.colorScheme.tertiary + appInfo.isInstalledOnOtherSource -> + stringResource(R.string.library_installed_elsewhere) to MaterialTheme.colorScheme.tertiary.copy(alpha = 0.7f) + !isSteam -> stringResource(R.string.library_status_ready) to MaterialTheme.colorScheme.tertiary + isInstalled -> stringResource(R.string.library_installed) to MaterialTheme.colorScheme.tertiary }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListCard.kt` around lines 246 - 254, Update the status-selection when expression in the LibraryListCard component so Steam-local statuses retain their existing precedence, but appInfo.isInstalledOnOtherSource is evaluated before the generic !isSteam ready fallback. Ensure non-Steam entries installed elsewhere display library_installed_elsewhere, while other non-Steam entries still display library_status_ready.
🧹 Nitpick comments (1)
.artifacts/33d120e5-dd12-4370-9d8f-04c5ef636724/implementation_plan.artifact.md (1)
47-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd automated coverage for the matching contract.
This logic drives every cross-store icon, installation flag, and navigation path; marking tests as N/A leaves regressions in name normalization, duplicate-source handling, and sibling installation detection to manual testing.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.artifacts/33d120e5-dd12-4370-9d8f-04c5ef636724/implementation_plan.artifact.md around lines 47 - 50, Add automated tests for the LibraryViewModel sibling-matching contract instead of marking verification as N/A. Cover name normalization, duplicate-source handling, and sibling installation detection, ensuring these cases validate the cross-store icon, installation flag, and navigation behavior driven by the matching logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.artifacts/33d120e5-dd12-4370-9d8f-04c5ef636724/walkthrough.artifact.md:
- Around line 35-36: Update the walkthrough’s Witcher example in the Core Logic
section to use names that match the current unaccented, lowercased, trimmed
normalization, or expand the sibling normalization to remove trademark and
symbol punctuation before grouping. Keep the documented behavior consistent with
the actual implementation.
In `@app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt`:
- Around line 963-968: Update the sibling filtering used to compute otherSources
and isInstalledOnOtherSource in LibraryViewModel so candidates must have both a
different appId and a different gameSource from entry. Preserve the existing
distinct source derivation and installation check after applying both
conditions.
In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt`:
- Around line 1221-1249: Update the alternate-source chip container in the
LibraryAppScreen layout around the otherSources iteration so chips wrap onto
multiple lines or are horizontally scrollable instead of being clipped by the
current non-scrolling Row. Preserve the existing chip appearance, spacing, and
onSourceClick behavior for every source.
In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt`:
- Around line 1108-1120: Update the onSourceClick handler and its ViewModel data
flow so alternate-source lookup uses the full unpaginated library data rather
than only the paged state.appInfoList. Expose target IDs or matching entries
through the existing lookup/API, then load/select the resolved target before
assigning selectedLibraryItem, while preserving the source and normalized-name
matching criteria.
---
Outside diff comments:
In
`@app/src/main/java/app/gamenative/ui/screen/library/components/LibraryGridCard.kt`:
- Around line 516-537: Update the check Icon inside GridStatusIcons so its
contentDescription uses library_installed_elsewhere when !isInstalled, while
retaining library_installed for locally installed items. Keep the existing icon
tint and rendering behavior unchanged.
In
`@app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListCard.kt`:
- Around line 246-254: Update the status-selection when expression in the
LibraryListCard component so Steam-local statuses retain their existing
precedence, but appInfo.isInstalledOnOtherSource is evaluated before the generic
!isSteam ready fallback. Ensure non-Steam entries installed elsewhere display
library_installed_elsewhere, while other non-Steam entries still display
library_status_ready.
---
Nitpick comments:
In
@.artifacts/33d120e5-dd12-4370-9d8f-04c5ef636724/implementation_plan.artifact.md:
- Around line 47-50: Add automated tests for the LibraryViewModel
sibling-matching contract instead of marking verification as N/A. Cover name
normalization, duplicate-source handling, and sibling installation detection,
ensuring these cases validate the cross-store icon, installation flag, and
navigation behavior driven by the matching logic.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 720b3df7-abeb-4569-883d-066a0e6f1231
📒 Files selected for processing (13)
.artifacts/33d120e5-dd12-4370-9d8f-04c5ef636724/implementation_plan.artifact.md.artifacts/33d120e5-dd12-4370-9d8f-04c5ef636724/task.artifact.md.artifacts/33d120e5-dd12-4370-9d8f-04c5ef636724/walkthrough.artifact.mdapp/src/main/java/app/gamenative/data/LibraryItem.ktapp/src/main/java/app/gamenative/ui/model/LibraryViewModel.ktapp/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.ktapp/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.ktapp/src/main/java/app/gamenative/ui/screen/library/components/LibraryAppItem.ktapp/src/main/java/app/gamenative/ui/screen/library/components/LibraryDetailPane.ktapp/src/main/java/app/gamenative/ui/screen/library/components/LibraryGridCard.ktapp/src/main/java/app/gamenative/ui/screen/library/components/LibraryListCard.ktapp/src/main/res/values/strings.xml
| onSourceClick = { targetSource -> | ||
| val normalizedName = selectedLibraryItem?.name?.unaccent()?.lowercase()?.trim() | ||
| // Search in ALL games, not just the paged list if possible. | ||
| // For now, we search in the current paged list which is likely to have sibling if they were linked. | ||
| val targetItem = state.appInfoList.find { | ||
| it.gameSource == targetSource && | ||
| it.name.unaccent().lowercase().trim() == normalizedName | ||
| } | ||
| if (targetItem != null) { | ||
| selectedAppId = targetItem.appId | ||
| selectedLibraryItem = targetItem | ||
| } | ||
| }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Resolve alternate sources outside the loaded page.
otherSources is derived before pagination, but this handler searches only the paged state.appInfoList. A game can advertise another owned source that is beyond the current page, then its chip cannot navigate anywhere. Expose sibling target IDs/all matching entries from the ViewModel (or a lookup API) and select/load the target before updating selectedLibraryItem.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt` around
lines 1108 - 1120, Update the onSourceClick handler and its ViewModel data flow
so alternate-source lookup uses the full unpaginated library data rather than
only the paged state.appInfoList. Expose target IDs or matching entries through
the existing lookup/API, then load/select the resolved target before assigning
selectedLibraryItem, while preserving the source and normalized-name matching
criteria.
There was a problem hiding this comment.
8 issues found across 13 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryAppItem.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryAppItem.kt:173">
P2: Store icons no longer inherit the surrounding content color: the default path explicitly supplies `Color.Unspecified`, which disables `Icon` tinting instead of using its default theme tint. On themed/dark surfaces, monochrome vector icons can render at their asset color (commonly black) and lose contrast; preserve `LocalContentColor` when no explicit tint is requested.</violation>
</file>
<file name=".artifacts/33d120e5-dd12-4370-9d8f-04c5ef636724/implementation_plan.artifact.md">
<violation number="1" location=".artifacts/33d120e5-dd12-4370-9d8f-04c5ef636724/implementation_plan.artifact.md:23">
P2: The sibling-linking logic iterates every entry and checks all siblings for each one, which scales poorly for large multi-store libraries. Consider building a normalized-name-to-metadata index (e.g., a Map of installed-by-source) once and reusing it, rather than recomputing per entry on every filter invocation.</violation>
<violation number="2" location=".artifacts/33d120e5-dd12-4370-9d8f-04c5ef636724/implementation_plan.artifact.md:49">
P1: The cross-store sibling matching logic (group by name, resolve otherSources, check install state across stores) is testable business logic in LibraryViewModel, not UI. Labeling it N/A for testing misses the opportunity to catch regressions in matching accuracy and install-state resolution. Add focused unit tests that cover normalization edge cases, duplicate matches, and cross-store install state detection.</violation>
</file>
<file name="app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt:1109">
P2: The sibling-lookup logic (unaccent/lowercase/trim normalization + list search + state mutation) is inlined in the composable, making it untestable and mixing UI with domain logic. Extract a ViewModel method like navigateToSibling(currentItem: LibraryItem, targetSource: GameSource) that returns the target item or appId, and call it from the composable instead of duplicating the normalization and search logic.</violation>
<violation number="2" location="app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt:1110">
P1: The "Also available on" source switcher silently fails when the target sibling game isn't on the currently loaded page. The `onSourceClick` handler only searches `state.appInfoList`, which is the current paged subset — if the sibling game from another store is on a different page, the `find` returns null, no navigation occurs, and the user sees no feedback. The code even acknowledges this in a comment but doesn't address it. Since cross-store linking is the headline feature of this PR, the switcher should either (a) search the full unfiltered list from the ViewModel rather than the paged subset, or (b) trigger a page navigation + retry, or (c) at minimum show a snackbar when the game isn't found.</violation>
</file>
<file name=".artifacts/33d120e5-dd12-4370-9d8f-04c5ef636724/walkthrough.artifact.md">
<violation number="1" location=".artifacts/33d120e5-dd12-4370-9d8f-04c5ef636724/walkthrough.artifact.md:23">
P3: Grammar: use "an" instead of "a" before "Also available on" since "Also" starts with a vowel sound.</violation>
<violation number="2" location=".artifacts/33d120e5-dd12-4370-9d8f-04c5ef636724/walkthrough.artifact.md:36">
P2: Name-only matching can produce false positive sibling links for games that happen to share the same normalized name but are different titles (remakes, regional variants, or unrelated games). Consider adding additional matching heuristics (e.g., edition keywords, release year proximity, or manual exclusion) to reduce false positives.</violation>
<violation number="3" location=".artifacts/33d120e5-dd12-4370-9d8f-04c5ef636724/walkthrough.artifact.md:36">
P3: The walkthrough claims that matching was verified between 'THE WITCHER 3' (Epic) and 'The Witcher® 3' (Steam) using the current `unaccent()` normalization. This example is misleading: the `unaccent()` function strips Unicode combining marks (accents) but does NOT handle symbols like `®` (registered trademark, U+00AE). The `®` symbol survives normalization, so these two names would NOT be linked as siblings. Consider updating the example to use a realistic matching pair (e.g., 'Pokémon' vs 'POKEMON') or adding a `removeNonAlphanumeric()` or `replace(symbolsRegex, "")` step to the normalization if symbol-stripping is intended.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
|
|
||
| ## Verification Plan | ||
|
|
||
| ### Automated Tests |
There was a problem hiding this comment.
P1: The cross-store sibling matching logic (group by name, resolve otherSources, check install state across stores) is testable business logic in LibraryViewModel, not UI. Labeling it N/A for testing misses the opportunity to catch regressions in matching accuracy and install-state resolution. Add focused unit tests that cover normalization edge cases, duplicate matches, and cross-store install state detection.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .artifacts/33d120e5-dd12-4370-9d8f-04c5ef636724/implementation_plan.artifact.md, line 49:
<comment>The cross-store sibling matching logic (group by name, resolve otherSources, check install state across stores) is testable business logic in LibraryViewModel, not UI. Labeling it N/A for testing misses the opportunity to catch regressions in matching accuracy and install-state resolution. Add focused unit tests that cover normalization edge cases, duplicate matches, and cross-store install state detection.</comment>
<file context>
@@ -0,0 +1,60 @@
+
+## Verification Plan
+
+### Automated Tests
+- N/A (UI-heavy changes), but I will verify that the `LibraryViewModel` logic correctly identifies siblings in a debug run if possible, or by careful manual verification of the state.
+
</file context>
| }, | ||
| onSourceClick = { targetSource -> | ||
| val normalizedName = selectedLibraryItem?.name?.unaccent()?.lowercase()?.trim() | ||
| // Search in ALL games, not just the paged list if possible. |
There was a problem hiding this comment.
P1: The "Also available on" source switcher silently fails when the target sibling game isn't on the currently loaded page. The onSourceClick handler only searches state.appInfoList, which is the current paged subset — if the sibling game from another store is on a different page, the find returns null, no navigation occurs, and the user sees no feedback. The code even acknowledges this in a comment but doesn't address it. Since cross-store linking is the headline feature of this PR, the switcher should either (a) search the full unfiltered list from the ViewModel rather than the paged subset, or (b) trigger a page navigation + retry, or (c) at minimum show a snackbar when the game isn't found.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt, line 1110:
<comment>The "Also available on" source switcher silently fails when the target sibling game isn't on the currently loaded page. The `onSourceClick` handler only searches `state.appInfoList`, which is the current paged subset — if the sibling game from another store is on a different page, the `find` returns null, no navigation occurs, and the user sees no feedback. The code even acknowledges this in a comment but doesn't address it. Since cross-store linking is the headline feature of this PR, the switcher should either (a) search the full unfiltered list from the ViewModel rather than the paged subset, or (b) trigger a page navigation + retry, or (c) at minimum show a snackbar when the game isn't found.</comment>
<file context>
@@ -1104,6 +1105,19 @@ private fun LibraryScreenContent(
},
+ onSourceClick = { targetSource ->
+ val normalizedName = selectedLibraryItem?.name?.unaccent()?.lowercase()?.trim()
+ // Search in ALL games, not just the paged list if possible.
+ // For now, we search in the current paged list which is likely to have sibling if they were linked.
+ val targetItem = state.appInfoList.find {
</file context>
| GameSource.GOG -> Icon(painter = painterResource(R.drawable.ic_gog), contentDescription = "Gog", modifier = Modifier.size(iconSize.dp).alpha(0.7f)) | ||
| GameSource.EPIC -> Icon(painter = painterResource(R.drawable.ic_epic), contentDescription = "Epic", modifier = Modifier.size(iconSize.dp).alpha(0.7f)) | ||
| GameSource.AMAZON -> Icon(imageVector = Icons.Filled.Amazon, contentDescription = "Amazon", modifier = Modifier.size(iconSize.dp).alpha(0.7f)) | ||
| GameSource.STEAM -> Icon(imageVector = Icons.Filled.Steam, contentDescription = "Steam", modifier = Modifier.size(iconSize.dp).alpha(alpha), tint = if (tint == Color.Unspecified) Color.Unspecified else tint) |
There was a problem hiding this comment.
P2: Store icons no longer inherit the surrounding content color: the default path explicitly supplies Color.Unspecified, which disables Icon tinting instead of using its default theme tint. On themed/dark surfaces, monochrome vector icons can render at their asset color (commonly black) and lose contrast; preserve LocalContentColor when no explicit tint is requested.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/ui/screen/library/components/LibraryAppItem.kt, line 173:
<comment>Store icons no longer inherit the surrounding content color: the default path explicitly supplies `Color.Unspecified`, which disables `Icon` tinting instead of using its default theme tint. On themed/dark surfaces, monochrome vector icons can render at their asset color (commonly black) and lose contrast; preserve `LocalContentColor` when no explicit tint is requested.</comment>
<file context>
@@ -158,24 +162,46 @@ fun GameSourceIcon(
- GameSource.GOG -> Icon(painter = painterResource(R.drawable.ic_gog), contentDescription = "Gog", modifier = Modifier.size(iconSize.dp).alpha(0.7f))
- GameSource.EPIC -> Icon(painter = painterResource(R.drawable.ic_epic), contentDescription = "Epic", modifier = Modifier.size(iconSize.dp).alpha(0.7f))
- GameSource.AMAZON -> Icon(imageVector = Icons.Filled.Amazon, contentDescription = "Amazon", modifier = Modifier.size(iconSize.dp).alpha(0.7f))
+ GameSource.STEAM -> Icon(imageVector = Icons.Filled.Steam, contentDescription = "Steam", modifier = Modifier.size(iconSize.dp).alpha(alpha), tint = if (tint == Color.Unspecified) Color.Unspecified else tint)
+ GameSource.CUSTOM_GAME -> Icon(imageVector = Icons.Filled.Folder, contentDescription = "Custom Game", modifier = Modifier.size(iconSize.dp).alpha(alpha), tint = if (tint == Color.Unspecified) Color.Unspecified else tint)
+ GameSource.GOG -> Icon(painter = painterResource(R.drawable.ic_gog), contentDescription = "Gog", modifier = Modifier.size(iconSize.dp).alpha(alpha), tint = if (tint == Color.Unspecified) Color.Unspecified else tint)
</file context>
| - Updated status text to show "Installed elsewhere" if applicable. | ||
|
|
||
| ### Game Detail Screen | ||
| - **[LibraryAppScreen.kt](file:///E:/workspace/StudioProjects/GameNative/app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt)**: |
There was a problem hiding this comment.
P3: Grammar: use "an" instead of "a" before "Also available on" since "Also" starts with a vowel sound.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .artifacts/33d120e5-dd12-4370-9d8f-04c5ef636724/walkthrough.artifact.md, line 23:
<comment>Grammar: use "an" instead of "a" before "Also available on" since "Also" starts with a vowel sound.</comment>
<file context>
@@ -0,0 +1,44 @@
+ - Updated status text to show "Installed elsewhere" if applicable.
+
+### Game Detail Screen
+- **[LibraryAppScreen.kt](file:///E:/workspace/StudioProjects/GameNative/app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt)**:
+ - Added a **"Also available on"** section in the game information area, allowing users to jump directly to the same game on a different store.
+ - Added a prominent **banner** that appears if you are viewing a store page for a game that is already installed via another source.
</file context>
| - Successfully built the `:app` module. | ||
|
|
||
| ### Core Logic | ||
| - Verified that games are matched using normalized names (removing accents and case-insensitive), ensuring consistent linking between stores like "THE WITCHER 3" (Epic) and "The Witcher® 3" (Steam). |
There was a problem hiding this comment.
P3: The walkthrough claims that matching was verified between 'THE WITCHER 3' (Epic) and 'The Witcher® 3' (Steam) using the current unaccent() normalization. This example is misleading: the unaccent() function strips Unicode combining marks (accents) but does NOT handle symbols like ® (registered trademark, U+00AE). The ® symbol survives normalization, so these two names would NOT be linked as siblings. Consider updating the example to use a realistic matching pair (e.g., 'Pokémon' vs 'POKEMON') or adding a removeNonAlphanumeric() or replace(symbolsRegex, "") step to the normalization if symbol-stripping is intended.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .artifacts/33d120e5-dd12-4370-9d8f-04c5ef636724/walkthrough.artifact.md, line 36:
<comment>The walkthrough claims that matching was verified between 'THE WITCHER 3' (Epic) and 'The Witcher® 3' (Steam) using the current `unaccent()` normalization. This example is misleading: the `unaccent()` function strips Unicode combining marks (accents) but does NOT handle symbols like `®` (registered trademark, U+00AE). The `®` symbol survives normalization, so these two names would NOT be linked as siblings. Consider updating the example to use a realistic matching pair (e.g., 'Pokémon' vs 'POKEMON') or adding a `removeNonAlphanumeric()` or `replace(symbolsRegex, "")` step to the normalization if symbol-stripping is intended.</comment>
<file context>
@@ -0,0 +1,44 @@
+- Successfully built the `:app` module.
+
+### Core Logic
+- Verified that games are matched using normalized names (removing accents and case-insensitive), ensuring consistent linking between stores like "THE WITCHER 3" (Epic) and "The Witcher® 3" (Steam).
+
+## How to Test
</file context>
| - Verified that games are matched using normalized names (removing accents and case-insensitive), ensuring consistent linking between stores like "THE WITCHER 3" (Epic) and "The Witcher® 3" (Steam). | |
| - Verified that games are matched using normalized names (removing accents and case-insensitive), ensuring consistent linking between stores like "POKEMON" (Epic) and "Pokémon" (Steam). |
- Add robust name normalization to strip symbols like ® and ™. - Refactor LibraryViewModel to identify sibling games from the full library data. - Fix logic error in InstallStatusBadge for non-Steam games. - Add accessibility labels for 'installed elsewhere' status in grid view. - Make 'Also available on' row horizontally scrollable in detail screen. - Add unit tests for the new normalization logic.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/main/java/app/gamenative/utils/StringUtils.kt`:
- Around line 9-21: The normalizeForComparison function currently removes
non-ASCII letters; update REGEX_NON_ALPHANUMERIC to use Unicode-aware
alphanumeric matching so scripts such as 英雄伝説 remain in comparison keys.
Preserve lowercasing and unaccenting behavior, and add a regression test
verifying non-ASCII titles produce non-empty normalized keys.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: eb7a20e3-7f3b-4ab0-85eb-a8f3af9b88b3
📒 Files selected for processing (6)
app/src/main/java/app/gamenative/ui/model/LibraryViewModel.ktapp/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.ktapp/src/main/java/app/gamenative/ui/screen/library/components/LibraryGridCard.ktapp/src/main/java/app/gamenative/ui/screen/library/components/LibraryListCard.ktapp/src/main/java/app/gamenative/utils/StringUtils.ktapp/src/test/java/app/gamenative/utils/StringUtilsTest.kt
🚧 Files skipped from review as they are similar to previous changes (3)
- app/src/main/java/app/gamenative/ui/screen/library/components/LibraryGridCard.kt
- app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt
- app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListCard.kt
| private val REGEX_NON_ALPHANUMERIC = Regex("[^a-zA-Z0-9]") | ||
|
|
||
| /** | ||
| * Normalizes a game title for comparison across different stores. | ||
| * Removes accents, converts to lowercase, strips non-alphanumeric characters, and trims. | ||
| * For example, "The Witcher® 3" and "the-witcher-3" will both normalize to "thewitcher3". | ||
| * | ||
| * @return The normalized string. | ||
| */ | ||
| fun CharSequence.normalizeForComparison(): String { | ||
| return this.unaccent() | ||
| .lowercase() | ||
| .replace(REGEX_NON_ALPHANUMERIC, "") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg 'StringUtils\.kt|LibraryViewModel\.kt|test.*StringUtils|StringUtils.*test' || true
echo "== StringUtils.kt =="
fd -a 'StringUtils\.kt' . | while read -r f; do
echo "--- $f"
wc -l "$f"
cat -n "$f"
done
echo "== occurrences =="
rg -n "normalizeForComparison|REGEX_NON_ALPHANUMERIC|Other source|installed|installed elsewhere|alternate" -S app || true
echo "== Kotlin tests outlines =="
fd -a '.+Test\.kt$' app | while read -r f; do
if rg -q "normalizeForComparison|StringUtils" "$f"; then
echo "--- $f"
wc -l "$f"
ast-grep outline "$f" || true
fi
doneRepository: utkarshdalal/GameNative
Length of output: 50380
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== LibraryViewModel outline =="
ast-grep outline app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt || true
echo "== LibraryViewModel relevant occurrences =="
rg -n "normalizeForComparison|group|Alternative|source|installed|Install|Same source|Other|elsewhere" app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt -C 3
echo "== StringUtilsTest =="
cat -n app/src/test/java/app/gamenative/utils/StringUtilsTest.kt
echo "== imports/usages of StringUtils.kt =="
rg -n "import app\.gamenative\.utils|import app\.gamenative\.utils\.StringUtils|import app\.gamenative\.utils\.\*\|normalizeForComparison" app/src/main/java app/src/test/java app/src/androidTest/java || true
echo "== behavioral test for existing regex vs Unicode classes =="
python3 - <<'PY'
import re
patterns = {
"ascii": re.compile(r"[^a-zA-Z0-9]"),
"unicode_letter_number": re.compile(r"[^\p{L}\p{N}]"),
}
samples = ["The Witcher® 3", "the-witcher-3", "英雄伝説", "Café", "Зима", "مرحبا", "123"]
# Python's \p is not enabled, so test ascii and a representative non-letter surrogate set.
ascii_re = re.compile(r"[^a-zA-Z0-9]", re.U)
for s in samples:
print(f"{s!r}: ascii -> {ascii_re.sub('', s)}")
PYRepository: utkarshdalal/GameNative
Length of output: 44699
Use Unicode alphanumeric character classes for comparison keys.
normalizeForComparison() currently strips non-ASCII letters from titles such as 英雄伝説, so distinct games can share an empty sibling key in LibraryViewModel. Build globalSiblingLookup with keys like 英雄伝説 instead of "", and add a regression test for non-ASCII scripts.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/java/app/gamenative/utils/StringUtils.kt` around lines 9 - 21,
The normalizeForComparison function currently removes non-ASCII letters; update
REGEX_NON_ALPHANUMERIC to use Unicode-aware alphanumeric matching so scripts
such as 英雄伝説 remain in comparison keys. Preserve lowercasing and unaccenting
behavior, and add a regression test verifying non-ASCII titles produce non-empty
normalized keys.
There was a problem hiding this comment.
2 issues found across 6 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt:633">
P2: Every library refresh now scans every custom folder, even when custom games are hidden, and GAME views scan them twice; large manual libraries can cause repeated filesystem and icon-extraction work during search/filter refreshes. Reuse a cached/single scan for both sibling lookup and visible custom entries, and avoid it when custom-source linking is unnecessary.
(Based on your team's feedback about scaling library and directory operations.) [FEEDBACK_USED]</violation>
<violation number="2" location="app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt:641">
P1: Distinct non-Latin-titled games are marked as available/installed on each other’s stores because their normalized names are all empty. Exclude blank normalization keys (or retain Unicode letters) before building the sibling map.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
|
|
||
| // Global mapping: normalized name -> List of siblings | ||
| val globalSiblingLookup = (steamEntriesForSiblings + gogEntriesForSiblings + epicEntriesForSiblings + amazonEntriesForSiblings + customEntriesForSiblings) | ||
| .groupBy({ it.first }, { it.second }) |
There was a problem hiding this comment.
P1: Distinct non-Latin-titled games are marked as available/installed on each other’s stores because their normalized names are all empty. Exclude blank normalization keys (or retain Unicode letters) before building the sibling map.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt, line 641:
<comment>Distinct non-Latin-titled games are marked as available/installed on each other’s stores because their normalized names are all empty. Exclude blank normalization keys (or retain Unicode letters) before building the sibling map.</comment>
<file context>
@@ -584,6 +599,47 @@ class LibraryViewModel @Inject constructor(
+
+ // Global mapping: normalized name -> List of siblings
+ val globalSiblingLookup = (steamEntriesForSiblings + gogEntriesForSiblings + epicEntriesForSiblings + amazonEntriesForSiblings + customEntriesForSiblings)
+ .groupBy({ it.first }, { it.second })
+
val steamOwnerTypeFiltered: List<SteamApp> = appList
</file context>
| .groupBy({ it.first }, { it.second }) | |
| .filter { it.first.isNotEmpty() } | |
| .groupBy({ it.first }, { it.second }) |
| } | ||
|
|
||
| // Scan all custom games for sibling detection | ||
| val customGameItemsAll = CustomGameScanner.scanAsLibraryItems() |
There was a problem hiding this comment.
P2: Every library refresh now scans every custom folder, even when custom games are hidden, and GAME views scan them twice; large manual libraries can cause repeated filesystem and icon-extraction work during search/filter refreshes. Reuse a cached/single scan for both sibling lookup and visible custom entries, and avoid it when custom-source linking is unnecessary.
(Based on your team's feedback about scaling library and directory operations.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt, line 633:
<comment>Every library refresh now scans every custom folder, even when custom games are hidden, and GAME views scan them twice; large manual libraries can cause repeated filesystem and icon-extraction work during search/filter refreshes. Reuse a cached/single scan for both sibling lookup and visible custom entries, and avoid it when custom-source linking is unnecessary.
(Based on your team's feedback about scaling library and directory operations.) </comment>
<file context>
@@ -584,6 +599,47 @@ class LibraryViewModel @Inject constructor(
+ }
+
+ // Scan all custom games for sibling detection
+ val customGameItemsAll = CustomGameScanner.scanAsLibraryItems()
+ val customEntriesForSiblings = customGameItemsAll.map { item ->
+ val normalizedName = item.name.normalizeForComparison()
</file context>
Description
This pull request introduces Cross-Store Game Awareness to the library, improving how the app handles games owned across multiple platforms (Steam, GOG, Epic, and Amazon).
Key enhancements:
LibraryViewModelnow identifies games with the same name across different sources using name normalization (accent removal and case-insensitive matching).These changes make the library feel more unified and help users manage their cross-platform collections more efficiently.
Type of Change
Checklist
#code-changes, I have discussed this change there and it has been green-lighted. If I do not have access, I have still provided clear context in this PR. If I skip both, I accept that this change may face delays in review, may not be reviewed at all, or may be closed.CONTRIBUTING.md.Summary by cubic
Adds cross-store awareness to the library so you can see and switch between Steam, GOG, Epic, and Amazon versions of the same game. Cards and detail pages now show all owned sources and when a game is installed via another store.
New Features
normalizeForComparison; build a global map inLibraryViewModelto populateotherSourcesandisInstalledOnOtherSourceinLibraryItem.Bug Fixes
InstallStatusBadgelogic for non-Steam games.Written for commit ca9af9b. Summary will update on new commits.
Summary by CodeRabbit