Skip to content

feat: native favorites with a context menu on any listed game - #1

Open
lepht wants to merge 2 commits into
mainfrom
feat/favorites
Open

feat: native favorites with a context menu on any listed game#1
lepht wants to merge 2 commits into
mainfrom
feat/favorites

Conversation

@lepht

@lepht lepht commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Favorites as a first-class list rather than a pak, with the add/remove action reachable from anywhere a game is listed.

Stacked on #20. This branch now sits on top of fix/atomic-writes, so saveFavorites() can use the openAtomic()/commitAtomic() helpers rather than duplicating them. Merge #20 first and the extra commit drops out of this diff on its own.

What this does

Press Y on any game the UI lists — a system folder, Recents, or a collection — and a context menu offers Add to / Remove from Favorites, plus Remove from Recents when that is where you are. A Favorites entry appears at the root and in the quick menu, alphabetized, and disappears again when the list is empty.

Storage mirrors recents: one SDCARD-relative path per line in .userdata/shared/.minui/favorites.txt, optionally followed by a tab and the display name it was favorited under. That keeps favorites working when a card moves between devices. Entries whose ROM has since been deleted are pruned on load. The file is rewritten atomically (write to .tmp, fsync, rename), so pulling the power mid-save can't truncate it.

A multi-disc game is keyed on its folder whether it was favorited from the browser (where it shows as a folder) or out of Recents (where it shows as the .m3u), so the same game can't land in the list twice under two names.

Why the list-rebuilding code exists

The root list and the faux directories are snapshots taken when they were opened, so a favorite toggle has to rebuild whichever of them has gone stale rather than waiting for a relaunch — the root list, the open list itself, and the quick menu row. Emptying Favorites from inside it backs out to the root, which also avoids rendering a zero-entry directory (see #6 for the underlying issue there).

UI note worth a second opinion

On a game row the right-hand hint group shows Y OPTIONS A OPEN instead of B BACK A OPEN — the group only holds two pairs. B still goes back, it just isn't spelled out on those rows. Easy to flip if you'd rather keep BACK visible and drop the OPTIONS hint.

Settings

Adds a Show Favorites toggle to Settings → Appearance next to Show Recents (favorites= in minuisettings.txt, default on). The quick menu ignores the toggle, matching how Recents and Collections already behave there.

Testing

Built the desktop target and drove it under Xvfb against a fake SD card (~25 ROMs across 7 systems, including a multi-disc PS1 folder), verifying each step by screenshot:

  • favoriting from a system folder and from Recents
  • the root entry and quick-menu tile appearing and disappearing live as the list fills and empties
  • removing the last favorite from inside the list backing out to a Favorites-free root
  • the Show Favorites toggle
  • pruning of favorites whose ROM was deleted
  • folder/.m3u canonicalization agreeing across both routes
  • launching a favorited multi-disc game (correct emulator command queued, correct recents entry written)
  • after the Write recent.txt atomically #20 rebase: re-verified favorites.txt on all three write paths — created from empty, grown by a second favorite, and shrunk by a removal — each landing intact with no .tmp residue

Also rebuilt under AddressSanitizer and re-ran the list-rebuild-while-open paths — the riskiest ones here — with zero errors. nextui.c passes a clean -fsyntax-only against both the tg5040 and tg5050 platform headers.

Not tested on hardware — cross-compiling needs the Docker toolchains, so device-side verification is still outstanding.

Known gap

Launching a favorited multi-disc game returns you to its system folder rather than to Favorites; plain ROMs return to Favorites correctly. Threading the faux path through openDirectory for the folder case would have widened the diff into shared navigation code for a small gain.

Unrelated bug spotted

ASan turned up a pre-existing self-overlapping copy in the settings code — filed as #7.

Refs LoveRetro#82, LoveRetro#631

Favorites are a first-class list rather than a separate pak: press Y on any
game the UI lists - in a system folder, Recents, or a collection - and a
context menu offers to add or remove it, plus to drop it from Recents when
that is where you are. The list shows up as "Favorites" at the root and in the
quick menu, alphabetized, and disappears again when it is empty.

They are stored like recents (one SDCARD-relative path per line in
.userdata/shared/.minui/favorites.txt, optionally with the display name), so
they survive a card moving between devices. Entries whose rom has since been
deleted are pruned on load. A multi-disc game is keyed on its folder whether it
was favorited from the browser or out of recents, so it can't land in the list
twice under two names.

That last part is canonicalGamePath, which lives in utils.c next to folderPath
and baseName rather than in nextui.c: it is path manipulation with no state
behind it, so it belongs with the other path helpers, and there it can be
tested. Doing so turned up that it took no output size and copied into 256
byte buffers with strcpy while callers can hand it a path of up to MAX_PATH -
a deep folder and a full No-Intro name reach 256 on a real card. It now takes
the buffer size, and says so rather than keying a favorite off a path it had
to truncate.

The root list and the faux directories are snapshots taken when they were
opened, so favoriting rebuilds whichever of them has gone stale rather than
waiting for a relaunch: the root list, the open list itself, the quick menu
row. Emptying Favorites from inside it backs out to the root.

Adds a "Show Favorites" toggle to Settings > Appearance alongside Show Recents,
and a Favorites icon to the shared res folder.

Refs LoveRetro#82, LoveRetro#631
Brings in the three upstream adoptions (#26, #27, #28) now on main.

One conflict, in the scroll-text block of nextui.c. Both sides edited
adjacent lines for unrelated reasons:

  - this branch added SCREEN_CONTEXTMENU to the outer screen exclusion list
  - #27 added a total>0 guard to the inner condition

Resolved as the union of both, which is the only correct answer here.
Taking ours alone drops total>0 and reopens #6 two lines above the
top->entries->items[top->selected] read; taking theirs alone drops the
context menu exclusion and renders scroll text over it.

Git auto-merged the other five guard sites #27 added. Verified after the
merge: no assert(entry) remains, every read of entries->items[top->selected]
sits behind a count or total>0 check, nextui.c is syntax-clean against the
desktop platform headers, and make test passes.
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