Skip to content

Add a Favourites tab and star toggle to the library - #1712

Open
jmarti326 wants to merge 18 commits into
utkarshdalal:masterfrom
jmarti326:feat/favourite-games-tab
Open

Add a Favourites tab and star toggle to the library#1712
jmarti326 wants to merge 18 commits into
utkarshdalal:masterfrom
jmarti326:feat/favourite-games-tab

Conversation

@jmarti326

@jmarti326 jmarti326 commented Jul 13, 2026

Copy link
Copy Markdown

Description

This adds a simple way to mark games as favourites so you can get back to them without scrolling the whole library. It follows the backlog card "Add Favourite Tag & tab in GN". I know new features are meant to go through #code-changes first, so please treat this as a starting point and I'm happy to change or drop any of it.

I kept it deliberately small, one star toggle rather than full tagging. Most of the effort went into making it behave well and feel right with both touch and a controller.

What it does:

  • Favourites are saved locally as a set of appId values, so they work across Steam, GOG, Epic, Amazon and custom games without needing a login.
  • A small FavoritesManager holds the set as a StateFlow so the tab and the cards stay in sync. It loads off the main thread and saves atomically, and if you star something while it is still loading that edit gets replayed on top instead of being lost.
  • FavoritesUtils keeps the pure logic (add, remove, filter, count) in one place with unit tests.
  • There is a Favourites tab with a count badge, plus an empty state so a fresh install shows a short "no favourites yet" message instead of a blank screen.
  • The star sits on the grid and list cards. It is reachable with a controller (focus ring), stays readable over cover art (a small scrim behind it), gives a little pop and a light haptic when you add, and has proper labels for screen readers.
  • The detail screen gets an Add/Remove favourite entry in the options menu, so every source has it in one place.
  • Removing a favourite shows an Undo, in case you hit the wrong star. Adding is silent. The toast clears itself after a few seconds.
  • Strings are translated for the locales that were already there.

A couple of things I am not sure about:

  • On the detail screen I reused the existing options menu instead of adding a visible star button, mostly to keep the diff small. If you would rather have a star up there, I can move it.
  • I went with a single star instead of tags because it felt like the smallest useful version. If the card was really about richer tagging, let me know and I will rework it.

Recording

Demo

Watch the screen recording (opens in GitHub's video viewer)

Screenshots

Favourites tab in grid view with a star on each card and the count in the tab
Favourites tab in grid view. Star on each card and the count in the tab.
Favourites tab in list view
Favourites tab in list view.
A favourite highlighted in the carousel view
A favourite highlighted in the carousel view.
The empty state shown before anything is starred
The empty state, before anything is starred.

Type of Change

  • Bug fix
  • Performance / stability improvement
  • Compatibility improvements
  • Other (requires prior approval)

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 a Favorites tab and a star toggle so you can mark games and find them fast across all sources. Adds an empty state and Undo for removals, with instant, safe updates even during initial load; the tab and badge stay in sync and snackbars now auto-dismiss after 6s.

  • New Features

    • Store favorites in PrefManager as a set of appIds; works for Steam, GOG, Epic, Amazon, and custom games without login.
    • FavoritesManager: exposes a StateFlow, loads off the main thread, uses atomic updates, and replays early edits so async loads never overwrite them.
    • FavoritesUtils: pure helpers (add/remove/filter/count/countPresent) with unit tests.
    • Favorites tab with a badge and a dedicated empty state (no favorites yet; filtered/unavailable). New strings localized; pref key favorite_app_ids (no migration).
    • Star on grid/list cards and Add/Remove Favorite in the detail menu; polished with focus ring, cover-art scrim, accessible labels, a spring “pop” on add, light haptics, and an Undo snackbar via SnackbarManager (now supports action + callback).
  • Bug Fixes

    • Badge count matches the Favorites tab (counts across credentialed sources, ignoring per-source visibility).
    • Prevent lost toggles during initial load by persisting under the same lock; closes a TOCTOU race.
    • Clamp pagination so removing favorites can’t leave the pager past the last page; avoid full re-filter when off the Favorites tab (badge updates only).
    • Detail options menu label reflects the current favorite state when reopened; hide the star on recommended items in list view to match the grid.
    • Log decode failures for stored favorites and fall back safely; fix Undo action layout in the custom SnackbarHost so the action renders, shares space correctly, and is reachable; remove unused FavoritesUtils.toggle.
    • Shorten snackbar display timeout to 6s.

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

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added persistent game favourites with a new Favorites library tab and a live favourites counter.
    • Added add/remove favourites actions in game option menus and a star button on library cards to toggle favourites.
    • Added localized favourites tab and action text across supported languages.
  • Tests
    • Added unit tests covering favourites add/remove/toggle, filtering (order preserved), and counting.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds persisted favourite game IDs, reactive favourite filtering and counts, favourite menu and card controls, a favourites library tab, utility tests, and localized strings.

Changes

Favourites feature

Layer / File(s) Summary
Persisted favourite state and utilities
app/src/main/java/app/gamenative/PrefManager.kt, app/src/main/java/app/gamenative/data/Favourites*.kt, app/src/test/java/app/gamenative/data/FavouritesUtilsTest.kt
Favourite IDs are stored as JSON, exposed through FavouritesManager, manipulated by pure utilities, and covered by unit tests.
Library filtering and counts
app/src/main/java/app/gamenative/ui/data/LibraryState.kt, app/src/main/java/app/gamenative/ui/enums/LibraryTab.kt, app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt, app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt
The favourites tab filters the combined library, refreshes when favourites change, and displays counts for enabled sources.
Favourite actions and star controls
app/src/main/java/app/gamenative/ui/enums/AppOptionMenuType.kt, app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt, app/src/main/java/app/gamenative/ui/screen/library/components/*
Add/remove actions are available in game menus, while grid and list cards render clickable favourite star controls.
Localized favourite strings
app/src/main/res/values*/strings.xml
Default and localized resources define favourites tab, menu, and add/remove action labels.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant FavouriteStarButton
  participant FavouritesManager
  participant PrefManager
  participant LibraryViewModel
  User->>FavouriteStarButton: tap star
  FavouriteStarButton->>FavouritesManager: toggle(appId)
  FavouritesManager->>PrefManager: persist favouriteAppIds
  FavouritesManager-->>LibraryViewModel: emit updated favourites
  LibraryViewModel->>LibraryViewModel: refilter library and recompute count
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 13.64% 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 clearly summarizes the main change: adding a Favourites tab and star toggle to the library.
Description check ✅ Passed The description includes the required sections and provides concrete details plus a recording link and checklist.
✨ 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.

@phobos665

Copy link
Copy Markdown
Contributor

Hey there, thanks for contributing.

How come you couldn't build locally?

@jmarti326

Copy link
Copy Markdown
Author

Hey there, thanks for contributing.

How come you couldn't build locally?

My environment got messed up, so I triggered a GitHub Action to generate a build while I worked through the issue. Everything is working now. Sorry for any confusion.

@jmarti326

jmarti326 commented Jul 13, 2026

Copy link
Copy Markdown
Author

Quick status update.

The feature is complete on my side and now has unit tests. I pulled the favourites logic (add, remove, toggle, filter and count) into a small FavouritesUtils helper, following the same idea as the existing LibrarySortUtils, so it can be tested on its own without touching storage or UI state. FavouritesManager and the library view model just call into it now, so nothing changes behaviour wise. Tests cover the toggle transitions and the library filter and count.

I ran the same unit test tasks the PR check uses (:app:testLegacyDebugUnitTest and :app:testModernDebugUnitTest) on my fork and both passed.

Happy to adjust anything if you would rather I take a different approach. Thanks for taking a look.

@jmarti326
jmarti326 marked this pull request as ready for review July 13, 2026 21:08
@jmarti326
jmarti326 requested a review from utkarshdalal as a code owner July 13, 2026 21:08

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

🤖 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/PrefManager.kt`:
- Around line 1274-1278: Update the JSON decoding catch block in the preferences
deserialization flow to log the caught exception with a one-line Timber.w call
before returning emptySet(). Preserve the existing fallback behavior and follow
the same logging pattern used by customGamePaths and customGameManualFolders.

In `@app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt`:
- Around line 172-180: Update the FavouritesManager.favourites observer to
serialize refilter operations by replacing collect with collectLatest, or
otherwise canceling the prior onFilterApps job before starting the next. Ensure
onFilterApps(paginationCurrentPage) cannot overlap and stale favourites
snapshots cannot overwrite the latest state.
- Around line 937-946: The favouritesCount calculation in LibraryViewModel
should use the same unfiltered source set as the FAVOURITES tab. Remove the
currentState.show*InLibrary conditions from the buildList while retaining each
source’s credential checks where applicable, so favourites from hidden library
sources remain included.

In
`@app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt`:
- Around line 724-738: Update getFavouriteOption to collect
FavouritesManager.favourites with collectAsStateWithLifecycle(), then derive
isFavourite by checking the collected set for libraryItem.appId. Use that
reactive flag for both the menu option type and toggle action so the label
updates when favourites change.

In
`@app/src/main/java/app/gamenative/ui/screen/library/components/FavouriteStarButton.kt`:
- Around line 5-6: Replace the invalid StarOutline import and its usage in
FavouriteStarButton with a supported AndroidX Compose star icon variant or an
existing custom vector asset, keeping the favourite-state visual behavior
intact.

In
`@app/src/main/java/app/gamenative/ui/screen/library/components/LibraryGridCard.kt`:
- Around line 311-318: Update the LibraryGridCard layout around
FavouriteStarButton so the star no longer overlays GridStatusIcons or
title/stats content. Place it in a dedicated bottom slot or reserve its full
clickable width in the existing layout, while preserving its bottom-end
alignment and current sizing.

In
`@app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListCard.kt`:
- Around line 202-205: Update ListViewCard so FavouriteStarButton is rendered
only when appInfo.isRecommended is false, matching the existing guard in
GridViewCard; keep the button’s current appId and iconSize unchanged.
🪄 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: 654c6418-7ecc-43a6-9769-7463bbc9bb46

📥 Commits

Reviewing files that changed from the base of the PR and between 1b68582 and 13c6705.

📒 Files selected for processing (29)
  • app/src/main/java/app/gamenative/PrefManager.kt
  • app/src/main/java/app/gamenative/data/FavouritesManager.kt
  • app/src/main/java/app/gamenative/data/FavouritesUtils.kt
  • app/src/main/java/app/gamenative/ui/data/LibraryState.kt
  • app/src/main/java/app/gamenative/ui/enums/AppOptionMenuType.kt
  • app/src/main/java/app/gamenative/ui/enums/LibraryTab.kt
  • app/src/main/java/app/gamenative/ui/model/LibraryViewModel.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/FavouriteStarButton.kt
  • app/src/main/java/app/gamenative/ui/screen/library/components/GameOptionsPanel.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-da/strings.xml
  • app/src/main/res/values-de/strings.xml
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values-fr/strings.xml
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values-ja/strings.xml
  • app/src/main/res/values-ko/strings.xml
  • app/src/main/res/values-pl/strings.xml
  • app/src/main/res/values-pt-rBR/strings.xml
  • app/src/main/res/values-ro/strings.xml
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/res/values-uk/strings.xml
  • app/src/main/res/values-zh-rCN/strings.xml
  • app/src/main/res/values-zh-rTW/strings.xml
  • app/src/main/res/values/strings.xml
  • app/src/test/java/app/gamenative/data/FavouritesUtilsTest.kt

Comment thread app/src/main/java/app/gamenative/PrefManager.kt
Comment on lines +172 to +180
// Re-filter whenever the set of favourite games changes, so the Favourites tab and the
// tab badge stay in sync as the user stars or unstars games.
viewModelScope.launch(Dispatchers.IO) {
FavouritesManager.favourites
.drop(1)
.collect {
onFilterApps(paginationCurrentPage)
}
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and inspect nearby declarations
ast-grep outline app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt --view expanded

echo
echo "---- focused lines ----"
sed -n '1,260p' app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt | nl -ba | sed -n '140,230p'

echo
echo "---- find onFilterApps and favourites flow usage ----"
rg -n "onFilterApps|FavouritesManager\.favourites|collectLatest|drop\(1\)" app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt app/src/main/java -S

Repository: utkarshdalal/GameNative

Length of output: 2605


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- favourites collector and nearby state ----"
sed -n '160,200p' app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt

echo
echo "---- onFilterApps implementation ----"
sed -n '500,690p' app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt

echo
echo "---- any Job handling around filtering ----"
rg -n "searchDebounceJob|Job\?|launch\(|cancel\(|onFilterApps\(" app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt -S

Repository: utkarshdalal/GameNative

Length of output: 13304


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- favourites references in the ViewModel ----"
rg -n "FavouritesManager|favourite|favorites" app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt -S

echo
echo "---- tail of onFilterApps ----"
sed -n '690,920p' app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt

Repository: utkarshdalal/GameNative

Length of output: 11927


Serialize favourites-triggered refilters. FavouritesManager.favourites.collect { onFilterApps(...) } can start overlapping viewModelScope jobs, and onFilterApps snapshots FavouritesManager.favourites.value before writing _state at the end. A slower earlier run can still finish last and replace the Favourites tab list/counts with a stale snapshot; switch this collector to collectLatest or cancel the previous filter job before launching a new one.

🤖 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/model/LibraryViewModel.kt` around lines
172 - 180, Update the FavouritesManager.favourites observer to serialize
refilter operations by replacing collect with collectLatest, or otherwise
canceling the prior onFilterApps job before starting the next. Ensure
onFilterApps(paginationCurrentPage) cannot overlap and stale favourites
snapshots cannot overwrite the latest state.

Comment thread app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt Outdated
Comment on lines +5 to +6
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.StarOutline

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
rg -n 'androidx\.compose\.material\.icons\.(filled|outlined)\.(StarOutline|Star)' app/src/main
rg -n 'material-icons(-extended)?' --glob 'build.gradle*' --glob 'libs.versions.toml' .

Repository: utkarshdalal/GameNative

Length of output: 1181


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n-- FavouriteStarButton.kt --\n'
sed -n '1,120p' app/src/main/java/app/gamenative/ui/screen/library/components/FavouriteStarButton.kt

printf '\n-- GameOptionsPanel.kt --\n'
sed -n '45,90p' app/src/main/java/app/gamenative/ui/screen/library/components/GameOptionsPanel.kt

printf '\n-- GameFeedbackDialog.kt --\n'
sed -n '1,70p' app/src/main/java/app/gamenative/ui/component/dialog/GameFeedbackDialog.kt

printf '\n-- libs.versions.toml (compose icons) --\n'
sed -n '45,60p' gradle/libs.versions.toml

Repository: utkarshdalal/GameNative

Length of output: 8691


🌐 Web query:

AndroidX Compose material icons filled StarOutline Icons.Filled.StarOutline reference material-icons-extended

💡 Result:

In Android Jetpack Compose, the standard androidx.compose.material:material-icons-core library and the androidx.compose.material:material-icons-extended library provide access to various Material icons [1][2]. Regarding the specific icon you requested: 1. Icons.Filled.StarOutline: This icon is typically not available directly as a standard Compose ImageVector constant in the Material library. The standard Icons.Filled.Star represents a filled star [1][3]. 2. Accessing Star Outline: To display a "star outline" in Compose, you have a few common options: - Use Icons.Outlined.Star: Note that in the standard Material Icons set, Icons.Outlined.Star is sometimes a filled star with an outline, rather than an empty star outline [4]. - Use Icons.Filled.StarBorder (if available in your version of the extended library) or import a custom SVG/Vector asset for the specific "star outline" look you desire [4]. - Manual Import: If the specific version of "Star Outline" you need is missing from the material-icons-extended library, the recommended approach is to download the SVG from the official Material Symbols website and import it as a Vector Asset into your Android Studio project [4][2]. 3. Material Icons Extended: While the material-icons-extended library includes a much larger set of icons than material-icons-core, it does not contain every single icon variation available in Material Symbols [2]. When an icon is missing from this dependency, adding a custom vector asset is the standard solution [4][2]. To use the extended icons, ensure you have the following dependency in your build.gradle file: implementation "androidx.compose.material:material-icons-extended:1.x.x" (replace with your compose version) [2].

Citations:


Use a valid Compose star icon. Icons.Filled.StarOutline isn’t part of the AndroidX Compose Material icon set, so this import/use site will fail to compile. Replace it with a supported icon variant or a custom vector asset.

🤖 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/FavouriteStarButton.kt`
around lines 5 - 6, Replace the invalid StarOutline import and its usage in
FavouriteStarButton with a supported AndroidX Compose star icon variant or an
existing custom vector asset, keeping the favourite-state visual behavior
intact.

Comment thread app/src/main/java/app/gamenative/ui/screen/library/components/LibraryGridCard.kt Outdated
Comment thread app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListCard.kt Outdated

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

All reported issues were addressed across 29 files

Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.

Re-trigger cubic

Comment thread app/src/main/java/app/gamenative/ui/screen/library/components/LibraryGridCard.kt Outdated
Comment thread app/src/main/java/app/gamenative/data/FavouritesManager.kt Outdated
Comment thread app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt Outdated
Comment thread app/src/main/java/app/gamenative/data/FavouritesManager.kt Outdated
Comment thread app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt Outdated
Comment thread app/src/main/java/app/gamenative/PrefManager.kt
Comment thread app/src/main/java/app/gamenative/ui/screen/library/components/LibraryListCard.kt Outdated
Comment thread app/src/main/java/app/gamenative/data/FavouritesUtils.kt Outdated

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

All reported issues were addressed across 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread app/src/main/java/app/gamenative/data/FavouritesManager.kt Outdated

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

🤖 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/data/FavouritesManager.kt`:
- Around line 41-61: Update the FavouritesManager init persistence flow and the
loaded setFavourite/toggle persistence path to perform PrefManager reads and
writes while holding lock. Use the live _favourites.value for comparisons and
writes rather than the captured merged snapshot, ensuring persistence decisions
are serialized with the merge and concurrent edits cannot be overwritten.
- Around line 41-61: The FavouritesManager init load must always mark the
manager as loaded even when PrefManager.favouriteAppIds throws. Update the
coroutine around the preference read to handle the failure or use a finally
block that sets loaded under lock, while preserving pending-edit application and
normal persistence behavior on successful reads.
🪄 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: 02ad5b23-0097-45dd-8493-4fc1eafc07e1

📥 Commits

Reviewing files that changed from the base of the PR and between 21d524c and abcd323.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/data/FavouritesManager.kt

Comment thread app/src/main/java/app/gamenative/data/FavoritesManager.kt

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

1 issue found across 2 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/data/FavouritesManager.kt">

<violation number="1" location="app/src/main/java/app/gamenative/data/FavouritesManager.kt:53">
P2: If the preference read (`PrefManager.favouriteAppIds`) throws — e.g., due to a corrupted DataStore or an I/O error — this `launch` aborts before `loaded = true` is reached. That leaves `setFavourite`/`toggle` permanently stuck on the pending-edits path with no persistence for the rest of the process. Consider wrapping the load in a try/catch that sets `loaded = true` in a `finally` block (failing open with an empty set), or adding a retry, so the system degrades gracefully.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread app/src/main/java/app/gamenative/data/FavouritesManager.kt Outdated
result = FavouritesUtils.apply(result, appId, favourite)
}
pendingEdits.clear()
loaded = true

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: If the preference read (PrefManager.favouriteAppIds) throws — e.g., due to a corrupted DataStore or an I/O error — this launch aborts before loaded = true is reached. That leaves setFavourite/toggle permanently stuck on the pending-edits path with no persistence for the rest of the process. Consider wrapping the load in a try/catch that sets loaded = true in a finally block (failing open with an empty set), or adding a retry, so the system degrades gracefully.

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/data/FavouritesManager.kt, line 53:

<comment>If the preference read (`PrefManager.favouriteAppIds`) throws — e.g., due to a corrupted DataStore or an I/O error — this `launch` aborts before `loaded = true` is reached. That leaves `setFavourite`/`toggle` permanently stuck on the pending-edits path with no persistence for the rest of the process. Consider wrapping the load in a try/catch that sets `loaded = true` in a `finally` block (failing open with an empty set), or adding a retry, so the system degrades gracefully.</comment>

<file context>
@@ -32,16 +32,31 @@ object FavouritesManager {
+                    result = FavouritesUtils.apply(result, appId, favourite)
+                }
+                pendingEdits.clear()
+                loaded = true
+                merged = result
+                _favourites.value = result
</file context>

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

All reported issues were addressed across 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread app/src/main/java/app/gamenative/data/FavouritesManager.kt Outdated

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

1 issue found across 30 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/PrefManager.kt">

<violation number="1" location="app/src/main/java/app/gamenative/PrefManager.kt:1270">
P1: Previously starred games disappear after updating because this new DataStore key does not read the existing `favourite_app_ids` value. Retain the established key or migrate its value to the renamed key before treating the new key as authoritative.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic


// Games the user has marked as favorite, stored as a set of LibraryItem.appId values so they
// work across every source (Steam, GOG, Epic, Amazon, custom games) without needing an account.
private val FAVORITE_APP_IDS = stringPreferencesKey("favorite_app_ids")

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: Previously starred games disappear after updating because this new DataStore key does not read the existing favourite_app_ids value. Retain the established key or migrate its value to the renamed key before treating the new key as authoritative.

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/PrefManager.kt, line 1270:

<comment>Previously starred games disappear after updating because this new DataStore key does not read the existing `favourite_app_ids` value. Retain the established key or migrate its value to the renamed key before treating the new key as authoritative.</comment>

<file context>
@@ -1265,21 +1265,21 @@ object PrefManager {
     // work across every source (Steam, GOG, Epic, Amazon, custom games) without needing an account.
-    private val FAVOURITE_APP_IDS = stringPreferencesKey("favourite_app_ids")
-    var favouriteAppIds: Set<String>
+    private val FAVORITE_APP_IDS = stringPreferencesKey("favorite_app_ids")
+    var favoriteAppIds: Set<String>
         get() {
</file context>

@jmarti326
jmarti326 force-pushed the feat/favourite-games-tab branch from 73a778f to 40ead44 Compare July 15, 2026 22:10
jmarti326 added 12 commits July 22, 2026 16:03
Adds a simple way to mark games as favourites and find them again quickly.

- Store favourites locally as a set of appIds, so they work across every
  source (Steam, GOG, Epic, Amazon, custom games) without needing a login
- Add FavouritesManager, which exposes the set as a StateFlow so the list
  and the game cards stay in sync as favourites change
- Add a Favourites tab to the library, with a count badge
- Add a star toggle on the list and grid cards
- Add an Add/Remove favourite entry to the game options menu on the detail
  screen, so it is available for every source in one place
- Add strings for the default locale and the existing translations
Move the pure favourites logic (add, remove, toggle, filter, count) into a small FavouritesUtils helper so it can be unit tested on its own, mirroring the existing LibrarySortUtils pattern. FavouritesManager and LibraryViewModel now delegate to it. Adds FavouritesUtilsTest covering the add/remove/toggle transitions and the library filter and count behaviour.
Use MutableStateFlow.updateAndGet so the read-modify-write happens in one step. This avoids a lost update if setFavourite is ever called from more than one thread, and we only write to PrefManager when the set actually changed.
Building the singleton no longer does a synchronous DataStore read in its initializer, so the first card or detail menu that touches it does not block the UI thread on disk. The saved set now loads on Dispatchers.IO and fills the StateFlow when it returns. A small guard skips that load if the user has already toggled a favourite, so an early edit cannot be overwritten.
The async load could drop previously saved favourites if the user starred a game before the disk read returned: the toggle applied on top of an empty set and then persisted. Pre-load edits are now recorded and replayed on top of the stored set once it loads, so nothing saved is lost.
The badge counted favourites using the user's per-source library visibility preferences, but the Favourites tab shows favourites from every source (gated only by credentials). If a source was hidden from the library the badge could undercount what the tab actually lists. The count now uses the same source logic as the tab.
The post-load persistence write happened outside the lock and compared against a stale snapshot, so a toggle racing with the initial load could be silently dropped. The flow update and the persist now happen together under the same lock in both the loader and setFavourite, so whichever holds the lock last writes the latest set.
Read the favourites set as Compose state in getFavouriteOption so the
Add/Remove label reflects the current state when the menu is reopened
after toggling, instead of a value captured once.
Recommended entries use synthetic ids that don't map to a real
favourite, so hide the star in the list card to match the grid card
which already skips it for recommended items.
Warn via Timber when the saved favourite ids can't be decoded instead of
silently returning an empty set, so a corrupt value is easier to notice
and diagnose.
Nothing calls FavouritesUtils.toggle; the toggle behaviour lives in
FavouritesManager, which builds on apply(). Drop the helper and its
test to avoid dead code.
Normalise the spelling to en-US: FavoritesManager/FavoritesUtils/
FavoriteStarButton, the PrefManager favoriteAppIds key, enum entries,
the FAVORITES tab, and the English strings. Non-English translations
keep their existing wording; only the string resource key names change.
The stored preference key becomes favorite_app_ids (the feature is not
yet released, so there is no saved data to migrate).
@jmarti326
jmarti326 force-pushed the feat/favourite-games-tab branch from 40ead44 to f477a3a Compare July 22, 2026 20:03
- Add a dedicated Favorites tab empty state with a "no favorites yet"
  variant (with a CTA back to All) and a filtered/unavailable variant,
  localized across all 14 supported languages.
- Expose FavoritesManager.loaded so the UI distinguishes "loaded empty"
  from "not yet loaded", publishing the loaded set before flipping the
  flag to avoid an empty-state/star flash.
- Only recompute the badge (not a full library re-filter) when favorites
  change while off the Favorites tab, removing needless rebuild/flicker.
- Clamp pagination to the valid range so removing favorites can't leave
  the pager on a page past the last one.
- Extract FavoritesUtils.countPresent and add unit tests.
Add controller/D-pad focus behavior, a legibility scrim on cover art, and
contextual accessibility labels to the favourite star button, plus an Undo
snackbar when a favourite is removed.

- SnackbarManager gains an optional action + callback (Undo); PluviaMain
  invokes the callback on SnackbarResult.ActionPerformed.
- FavoriteStarButton now owns its interaction source and draws a focusRing,
  sits on a translucent circular scrim when overlaid on cover art (48dp touch
  target preserved), and uses named a11y labels (Add/Remove <game>).
- Removal from the grid/list star and the options menu shows an Undo snackbar
  that re-adds the game.
- New strings (favorite_add_named, favorite_remove_named, favorite_removed,
  favorite_removed_named, undo) localized across all 14 locales.
- FavoriteStarButton plays a spring 'pop' scale when a game is favourited
  (only on user toggles, not when an already-favourited card scrolls in) and
  fires haptic feedback on tap.
- Halve the snackbar display cap (SNACKBAR_SHOW_TIMEOUT_MS) from 15s to 7.5s
  so toasts, including the favourite Undo, dismiss faster.
Address issues found in review of the favourites polish:

- The custom SnackbarHost only rendered the message Text, so the Undo action
  was never shown or reachable (touch, controller, or screen reader). Render
  the action as a focusable TextButton that calls performAction().
- The pop animation's 'first composition' guard was defeated by the async
  favorites load: every already-favourited card popped at once when the set
  finished loading. Gate the animation on FavoritesManager.loaded and only
  animate a genuine false->true transition observed after load.
- Replace the heavy LongPress haptic on the toggle with a light CONTEXT_CLICK
  tick, which better suits a quick favourite toggle.

@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 28 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/screen/library/components/FavoriteActions.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/components/FavoriteActions.kt:18">
P1: A cold-start tap can silently remove a stored favorite: the UI and `wasFavorite` can still be false when loading flips the manager to true, then `toggle()` removes it without showing Undo. Apply the state observed for this click rather than reading the manager again.</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/screen/library/components/FavoriteStarButton.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/components/FavoriteStarButton.kt:87">
P3: Quickly removing a just-added favorite can leave the outlined star permanently scaled down until the card is recreated. Reset `scale` when the state becomes false so cancellation of the pop animation cannot preserve its intermediate value.</violation>

<violation number="2" location="app/src/main/java/app/gamenative/ui/screen/library/components/FavoriteStarButton.kt:127">
P3: The image-overlay scrim ignores theme-provided `scrim` color, so custom color schemes cannot keep this control visually consistent. Use the shared Material color instead of a hardcoded black.

(Based on your team's feedback about hardcoded UI colors.) [FEEDBACK_USED].</violation>
</file>

<file name="app/src/main/res/values-ru/strings.xml">

<violation number="1" location="app/src/main/res/values-ru/strings.xml:2042">
P3: The removal snackbar can be grammatically incorrect for many game titles because `удалено` must agree with `%1$s`. Use an impersonal construction that places the title after the action.</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/util/SnackbarManager.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/util/SnackbarManager.kt:26">
P3: An action callback can be silently dropped when a caller passes `null` for `actionLabel`, because the snackbar host renders no action control in that case. Make the action-overload label non-null so every accepted callback is reachable.</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/PluviaMain.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/PluviaMain.kt:1592">
P2: Undo becomes unreachable if the Manage Mods overlay takes ownership while its snackbar is visible: `NexusDialogSnackbarHost` renders only the message. Render the action there too, or share this action-capable snackbar renderer across hosts.</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:998">
P2: Controller users cannot reach “Browse all games” from an empty Favorites tab: DPAD_DOWN from the tab bar is consumed, but empty lists are not considered focusable. Route empty-state down navigation to the action (or focus it explicitly) so the recovery action is usable without touch.</violation>
</file>

<file name="app/src/main/java/app/gamenative/data/FavoritesManager.kt">

<violation number="1" location="app/src/main/java/app/gamenative/data/FavoritesManager.kt:62">
P3: Cold-start saved favorites can still briefly render as unstarred/empty and trigger the star-pop path because the two independent flows do not provide an atomic snapshot. Expose and collect one load-state value containing both the ids and readiness flag, rather than using `loaded` as a separate completion signal.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

*/
internal fun toggleFavoriteWithUndo(context: Context, appId: String, gameName: String?) {
val wasFavorite = FavoritesManager.isFavorite(appId)
FavoritesManager.toggle(appId)

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: A cold-start tap can silently remove a stored favorite: the UI and wasFavorite can still be false when loading flips the manager to true, then toggle() removes it without showing Undo. Apply the state observed for this click rather than reading the manager again.

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/FavoriteActions.kt, line 18:

<comment>A cold-start tap can silently remove a stored favorite: the UI and `wasFavorite` can still be false when loading flips the manager to true, then `toggle()` removes it without showing Undo. Apply the state observed for this click rather than reading the manager again.</comment>

<file context>
@@ -0,0 +1,31 @@
+ */
+internal fun toggleFavoriteWithUndo(context: Context, appId: String, gameName: String?) {
+    val wasFavorite = FavoritesManager.isFavorite(appId)
+    FavoritesManager.toggle(appId)
+    if (wasFavorite) {
+        val message = if (gameName.isNullOrBlank()) {
</file context>

color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyMedium,
)
if (actionLabel != null) {

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: Undo becomes unreachable if the Manage Mods overlay takes ownership while its snackbar is visible: NexusDialogSnackbarHost renders only the message. Render the action there too, or share this action-capable snackbar renderer across hosts.

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/PluviaMain.kt, line 1592:

<comment>Undo becomes unreachable if the Manage Mods overlay takes ownership while its snackbar is visible: `NexusDialogSnackbarHost` renders only the message. Render the action there too, or share this action-capable snackbar renderer across hosts.</comment>

<file context>
@@ -1566,12 +1574,31 @@ fun PluviaMain(
+                                    color = MaterialTheme.colorScheme.onSurface,
+                                    style = MaterialTheme.typography.bodyMedium,
+                                )
+                                if (actionLabel != null) {
+                                    TextButton(onClick = { data.performAction() }) {
+                                        Text(
</file context>

Comment thread app/src/main/java/app/gamenative/ui/PluviaMain.kt Outdated
)
} else if (showFavoritesEmptyState) {
if (favorites.isEmpty()) {
LibraryFavoritesEmptyState(

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: Controller users cannot reach “Browse all games” from an empty Favorites tab: DPAD_DOWN from the tab bar is consumed, but empty lists are not considered focusable. Route empty-state down navigation to the action (or focus it explicitly) so the recovery action is usable without touch.

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

<comment>Controller users cannot reach “Browse all games” from an empty Favorites tab: DPAD_DOWN from the tab bar is consumed, but empty lists are not considered focusable. Route empty-state down navigation to the action (or focus it explicitly) so the recovery action is usable without touch.</comment>

<file context>
@@ -984,6 +993,24 @@ private fun LibraryScreenContent(
                     )
+                } else if (showFavoritesEmptyState) {
+                    if (favorites.isEmpty()) {
+                        LibraryFavoritesEmptyState(
+                            titleResId = R.string.favorites_empty_title,
+                            messageResId = R.string.favorites_empty_message,
</file context>

<string name="favorite_add_named">Добавить %1$s в избранное</string>
<string name="favorite_remove_named">Удалить %1$s из избранного</string>
<string name="favorite_removed">Удалено из избранного</string>
<string name="favorite_removed_named">%1$s удалено из избранного</string>

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 removal snackbar can be grammatically incorrect for many game titles because удалено must agree with %1$s. Use an impersonal construction that places the title after the action.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/res/values-ru/strings.xml, line 2042:

<comment>The removal snackbar can be grammatically incorrect for many game titles because `удалено` must agree with `%1$s`. Use an impersonal construction that places the title after the action.</comment>

<file context>
@@ -2027,8 +2027,18 @@ https://gamenative.app
+    <string name="favorite_add_named">Добавить %1$s в избранное</string>
+    <string name="favorite_remove_named">Удалить %1$s из избранного</string>
+    <string name="favorite_removed">Удалено из избранного</string>
+    <string name="favorite_removed_named">%1$s удалено из избранного</string>
+    <string name="undo">Отменить</string>
 </resources>
</file context>
Suggested change
<string name="favorite_removed_named">%1$s удалено из избранного</string>
<string name="favorite_removed_named">Удалено из избранного: %1$s</string>


fun show(message: String) = show(Event(message))

fun show(message: String, actionLabel: String?, onAction: () -> Unit) =

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: An action callback can be silently dropped when a caller passes null for actionLabel, because the snackbar host renders no action control in that case. Make the action-overload label non-null so every accepted callback is reachable.

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/util/SnackbarManager.kt, line 26:

<comment>An action callback can be silently dropped when a caller passes `null` for `actionLabel`, because the snackbar host renders no action control in that case. Make the action-overload label non-null so every accepted callback is reachable.</comment>

<file context>
@@ -8,11 +8,26 @@ import kotlinx.coroutines.flow.receiveAsFlow
+
+    fun show(message: String) = show(Event(message))
+
+    fun show(message: String, actionLabel: String?, onAction: () -> Unit) =
+        show(Event(message, actionLabel, onAction))
+
</file context>

// 48dp touch target the surrounding IconButton already provides.
Box(
modifier = Modifier
.background(Color.Black.copy(alpha = 0.32f), CircleShape)

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 image-overlay scrim ignores theme-provided scrim color, so custom color schemes cannot keep this control visually consistent. Use the shared Material color instead of a hardcoded black.

(Based on your team's feedback about hardcoded UI colors.) .

View Feedback

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/FavoriteStarButton.kt, line 127:

<comment>The image-overlay scrim ignores theme-provided `scrim` color, so custom color schemes cannot keep this control visually consistent. Use the shared Material color instead of a hardcoded black.

(Based on your team's feedback about hardcoded UI colors.) .</comment>

<file context>
@@ -41,17 +65,72 @@ internal fun FavoriteStarButton(
+            // 48dp touch target the surrounding IconButton already provides.
+            Box(
+                modifier = Modifier
+                    .background(Color.Black.copy(alpha = 0.32f), CircleShape)
+                    .padding(4.dp),
+            ) {
</file context>
Suggested change
.background(Color.Black.copy(alpha = 0.32f), CircleShape)
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.32f), CircleShape)

if (!loaded) return@LaunchedEffect
val previous = lastFavoriteState
lastFavoriteState = isFavorite
if (previous == false && isFavorite) {

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: Quickly removing a just-added favorite can leave the outlined star permanently scaled down until the card is recreated. Reset scale when the state becomes false so cancellation of the pop animation cannot preserve its intermediate value.

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/FavoriteStarButton.kt, line 87:

<comment>Quickly removing a just-added favorite can leave the outlined star permanently scaled down until the card is recreated. Reset `scale` when the state becomes false so cancellation of the pop animation cannot preserve its intermediate value.</comment>

<file context>
@@ -41,17 +65,72 @@ internal fun FavoriteStarButton(
+        if (!loaded) return@LaunchedEffect
+        val previous = lastFavoriteState
+        lastFavoriteState = isFavorite
+        if (previous == false && isFavorite) {
+            scale.snapTo(0.6f)
+            scale.animateTo(
</file context>

// to `loaded` never sees `true` while `favorites` is still the initial empty set
// (which would briefly render the "no favorites yet" empty state).
_favorites.value = result
_loaded.value = true

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: Cold-start saved favorites can still briefly render as unstarred/empty and trigger the star-pop path because the two independent flows do not provide an atomic snapshot. Expose and collect one load-state value containing both the ids and readiness flag, rather than using loaded as a separate completion signal.

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/data/FavoritesManager.kt, line 62:

<comment>Cold-start saved favorites can still briefly render as unstarred/empty and trigger the star-pop path because the two independent flows do not provide an atomic snapshot. Expose and collect one load-state value containing both the ids and readiness flag, rather than using `loaded` as a separate completion signal.</comment>

<file context>
@@ -47,8 +55,11 @@ object FavoritesManager {
+                // to `loaded` never sees `true` while `favorites` is still the initial empty set
+                // (which would briefly render the "no favorites yet" empty state).
                 _favorites.value = result
+                _loaded.value = true
                 // Persist inside the lock so a concurrent toggle cannot be overwritten by a stale
                 // snapshot written after the lock is released.
</file context>

The message text was unconstrained, so it consumed the row width and squeezed
the Undo action into a vertical sliver (or pushed it off-screen for long game
names). Make the snackbar fill width with side margins, give the message a
weight so it shares space and ellipsizes at two lines, and keep the action
label on a single line so Undo always renders at its natural width.
7.5s felt too long; reduce SNACKBAR_SHOW_TIMEOUT_MS to 6000ms.
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.

2 participants