Skip to content

feat(library): add cross-store awareness and source switcher - #1760

Open
Meloon33 wants to merge 2 commits into
utkarshdalal:masterfrom
Meloon33:feat/library-ui-cross-store-info
Open

feat(library): add cross-store awareness and source switcher#1760
Meloon33 wants to merge 2 commits into
utkarshdalal:masterfrom
Meloon33:feat/library-ui-cross-store-info

Conversation

@Meloon33

@Meloon33 Meloon33 commented Jul 22, 2026

Copy link
Copy Markdown

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:

  • Automatic Sibling Linking: The LibraryViewModel now identifies games with the same name across different sources using name normalization (accent removal and case-insensitive matching).
  • Multi-Store Visibility: Game cards in the library grid and list views now display icons for all stores where the user owns the game, providing a clear overview of platform availability.
  • Enhanced Installation Tracking: The "Installed" indicator now accounts for cross-store installations. If a game is installed via a different store, it shows an "Installed elsewhere" status, helping users avoid redundant downloads.
  • In-App Store Switcher: A new "Also available on" section has been added to the game detail screen, allowing for instant navigation between different store versions of the same game.
  • Ownership Banner: When viewing a game that isn't installed in the current store but is already installed via another, a prominent banner informs the user to prevent duplicate installations.

These changes make the library feel more unified and help users manage their cross-platform collections more efficiently.

Type of Change

  • Bug fix
  • Performance / stability improvement
  • Compatibility improvements
  • Other (UI/UX enhancement: Library cross-store awareness)

Checklist

  • If I have access to #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.
  • This change aligns with the current project scope (core functionality, stability, or performance). If not, it has been explicitly approved beforehand.
  • I have attached a recording of the change.
  • I have read and agree to the contribution guidelines in 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

    • Link sibling games using robust normalizeForComparison; build a global map in LibraryViewModel to populate otherSources and isInstalledOnOtherSource in LibraryItem.
    • Show other store icons on grid/list cards and a dimmed checkmark when “Installed elsewhere”.
    • Add an “Also available on” section with a source switcher; row is horizontally scrollable.
    • Display a banner when viewing a store page for a game already installed via another source.
  • Bug Fixes

    • Fix InstallStatusBadge logic for non-Steam games.
    • Add accessibility labels for the “Installed elsewhere” status.
    • Add unit tests for the new normalization logic.

Written for commit ca9af9b. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added cross-store game awareness across Steam, GOG, Epic, and Amazon by matching games using accent/case- and punctuation-insensitive titles.
    • Library cards now show icons for other available stores.
    • Updated installed-state messaging and badges to indicate when a game is installed elsewhere.
    • Game details now include an “Also available on” section with store navigation and an “already installed” banner when applicable.
  • Tests
    • Added coverage for the new cross-store title normalization behavior.

@Meloon33
Meloon33 requested a review from utkarshdalal as a code owner July 22, 2026 22:21
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds 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.

Changes

Cross-store game awareness

Layer / File(s) Summary
Cross-store metadata and matching
.artifacts/.../*, app/src/main/java/app/gamenative/data/LibraryItem.kt, app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt, app/src/main/java/app/gamenative/utils/StringUtils.kt, app/src/test/java/app/gamenative/utils/StringUtilsTest.kt
Library entries now track alternate sources and cross-store installation state. Entries with normalized, accent-insensitive names are linked as siblings, with normalization tests added.
Library source indicators
app/src/main/java/app/gamenative/ui/screen/library/components/LibraryAppItem.kt, .../LibraryGridCard.kt, .../LibraryListCard.kt, app/src/main/res/values/strings.xml
Grid and list cards display alternate store icons and distinguish locally installed games from games installed through another source.
Alternate-source detail navigation
app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt, LibraryScreen.kt, appscreen/BaseAppScreen.kt, components/LibraryDetailPane.kt
Detail screens show installed-elsewhere messaging and alternate-store entries; selecting a source locates and displays the matching library item.

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
Loading

Possibly related PRs

Suggested reviewers: utkarshdalal

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% 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
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.
Title check ✅ Passed The title is concise and accurately summarizes the main change: cross-store awareness plus a source switcher.
Description check ✅ Passed Matches the template well with Description, Type of Change, and Checklist sections; only the Recording/GIF section is missing.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

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

@coderabbitai coderabbitai Bot left a comment

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.

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 win

Use an “installed elsewhere” accessibility label.

When only isInstalledOnOtherSource is true, the check icon still exposes library_installed, telling assistive technologies that the current store’s version is installed locally. Use library_installed_elsewhere when !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 win

Make installed-elsewhere reachable for non-Steam entries.

!isSteam matches every GOG, Epic, Amazon, and custom entry before Line 253, so those cards always show “Ready.” Because non-Steam entries also initialize isInstalled = true at Lines 224-230, merely moving the new branch below isInstalled would 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 win

Add 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7127305 and 5d899a5.

📒 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.md
  • app/src/main/java/app/gamenative/data/LibraryItem.kt
  • app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt
  • app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/library/components/LibraryAppItem.kt
  • app/src/main/java/app/gamenative/ui/screen/library/components/LibraryDetailPane.kt
  • app/src/main/java/app/gamenative/ui/screen/library/components/LibraryGridCard.kt
  • app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListCard.kt
  • app/src/main/res/values/strings.xml

Comment thread app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt Outdated
Comment on lines +1108 to +1120
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
}
},

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.

🎯 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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

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

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.

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.

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.

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)

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.

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>

Comment thread app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt Outdated
Comment thread app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt
Comment thread app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt Outdated
- 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)**:

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.

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).

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.

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>
Suggested change
- 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.

@coderabbitai coderabbitai Bot left a comment

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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5d899a5 and ca9af9b.

📒 Files selected for processing (6)
  • app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt
  • app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/library/components/LibraryGridCard.kt
  • app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListCard.kt
  • app/src/main/java/app/gamenative/utils/StringUtils.kt
  • app/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

Comment on lines +9 to +21
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, "")

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.

🎯 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
done

Repository: 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)}")
PY

Repository: 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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

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 })

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.

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>
Suggested change
.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()

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.

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.)

View Feedback: 1 2

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>

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