Skip to content

feat(browse): add a blacklist to hide titles from Browse - #410

Open
SomeGuz wants to merge 1 commit into
towerwatchman:nightlyfrom
SomeGuz:feat/browse-blacklist
Open

SomeGuz wants to merge 1 commit into
towerwatchman:nightlyfrom
SomeGuz:feat/browse-blacklist

Conversation

@SomeGuz

@SomeGuz SomeGuz commented Sep 17, 2026

Copy link
Copy Markdown

What this changes

Adds a Browse blacklist. A title can be blacklisted from its right-click menu or with a new Blacklist button next to Add to Wishlist on its detail page. It is then hidden from Browse and stays hidden across restarts. Settings > Blacklist lists every blacklisted title and has a Remove button that brings it back.

Why

Browse has no way to get rid of a title you never want to see again; it keeps coming back in every scroll and search.

Design points:

  • Exclusion is in SQL. It is part of the shared catalog WHERE clause on both query paths: buildIndexWhere in catalogIndex.js, and getCatalogGamesFromUnion in versions.js, which serves index-not-ready, updateAvailable and fast-path failures. Both use one builder, electron/db/blacklistSql.js, so they cannot drift. Filtering a fetched page instead would size the scrollbar for rows that never render and leave holes in the sparse grid.
  • No index rebuild. Blacklist state is joined at query time, the same way the existing local-only state is.
  • Matching is by provider id (atlas / f95 / lc / steam / gog), one indexed NOT EXISTS per column, in the same style as wishlistOnly. resolveMissingIds fills in sibling ids first, so blacklisting the F95 tile also hides the same game's Atlas and LewdCorner tiles.
  • Installed titles are never hidden, and neither the context menu nor the detail page offers Blacklist on them.
  • Wishlist interaction. Blacklisting a wishlisted title also removes it from the wishlist.
  • Refresh path. Every blacklist write broadcasts blacklist-updated, and the main window refetches Browse from the first page. Patching the sparse array in place would shift every loaded page. The Settings list refreshes from the same event, so each window reflects changes made in the other.

How it was tested

  • tests/browse-blacklist.test.js runs against a real sqlite DB:

    • add / list / remove;
    • dedupe;
    • GOG-only keying;
    • blacklisting removes the wishlist entry;
    • entries survive re-opening the same data dir (restart);
    • the title is hidden and total drops on both the union path and the index path;
    • removing the entry brings the title back without an index rebuild.
    • Each path's test was confirmed to fail with that path's exclusion clause removed.
  • tests/context-menu.test.js:

    • Browse rows get Blacklist with the gog id in the payload, and the dispatch cannot be hijacked.
    • Local and installed rows don't get it.
    • The "every action has a case in handleContextAction" check now also walks a catalog row.
    • Existing exact-label expectations for catalog rows were updated to include Blacklist.
  • tests/context-action-result.test.js: blacklistGame writes and broadcasts; a failure is returned and not broadcast.

  • tests/blacklist-action-bar.test.jsx: the button renders when allowed, routes the click, and is disabled while busy.

  • tests/blacklist-settings.test.jsx: the tab is visible; the section lists entries; Remove sends the row back and re-reads; empty state; refresh on a change from another window.

  • Added tests that cover this change

  • For a fix: the regression test fails against the unfixed code (not a fix; the exclusion tests were still confirmed red without the clause)

  • npm run check passes locally

  • New functions and IPC handlers have comments explaining why

  • CHANGELOG.md updated

  • This PR targets nightly, not main

AI assistance

  • No AI was used on this change
  • AI was used on this change

Tool and model: Claude Opus 5 (claude-opus-5) via Claude Code

What it wrote: All of it:

  • New files:
    • electron/db/blacklist.js
    • electron/db/blacklistSql.js
    • src/components/settings/BlacklistSettings.jsx
    • all three new test files
  • Edits:
    • electron/db/index.js (table and indexes)
    • electron/db/catalogIndex.js and electron/db/versions.js (exclusion)
    • electron/db/wishlist.js (one added export)
    • electron/ipc/games.js and electron/ipc/windows.js
    • electron/preload.js
    • src/App.jsx
    • GameDetailPage.jsx and ActionBar.jsx
    • gameContextMenu.js
    • Settings.jsx and settingsIcons.js
    • the two existing test files
    • CHANGELOG.md

What you verified yourself: The assistant ran npm run check (lint clean with no new warnings, 124 files / 1389 tests passing, renderer build OK) and confirmed that the two exclusion tests fail with their clause removed. I tested a Linux AppImage built from this branch by hand and confirmed:

  • blacklisting from the right-click menu and from the detail-page button hides the title from Browse;
  • blacklisted titles are still hidden after restarting Atlas;
  • Settings > Blacklist lists them, and Remove brings a title back in Browse;
  • blacklisting a wishlisted title removes it from the wishlist;
  • I looked at how the scroll position behaves after blacklisting deep in the grid.

IPC changes

Added, both sides in this PR:

  • blacklist-add, blacklist-remove and blacklist-list: ipcMain.handle in electron/ipc/games.jsipcRenderer.invoke in electron/preload.js.
  • blacklist-updated: sent from electron/ipc/games.js (add/remove handlers) and electron/ipc/windows.js (blacklistGame context action) ↔ ipcRenderer.on in preload.js, consumed by App.jsx and BlacklistSettings.jsx.
  • New run-context-action case: blacklistGame.

⚠️ Preload / security boundary:

  • electron/preload.js gains four electronAPI entries: addBlacklistEntry, removeBlacklistEntry, getBlacklistEntries and onBlacklistUpdated. The last returns its own unsubscribe.
  • The removeAllListeners allowlist is not widened.

Anything the reviewer should know

  • Scroll position. After a blacklist, Browse refetches from page 0. This can reset the scroll position when blacklisting deep in the grid. I looked at it by hand in the AppImage; a reviewer with a large catalog may want to check it too.
  • Titles with no provider id can be stored but cannot be matched by the SQL exclusion, so they stay visible. This should be rare for catalog rows.
  • Wishlist view. Wishlist-view rows are offered Blacklist too: the isWishlistEntry flag was removed on nightly, and blacklisting from there has a visible effect because it removes the wishlist entry.
  • Button colour. The Blacklist button uses the existing --color-danger token rather than a new --color-detail-* variable, so existing custom themes don't render it transparent.
  • Settings tab visibility. The tab is always visible, even when Browse / NSFW is disabled; it just shows the empty state.

🤖 Generated with Claude Code

Titles can be blacklisted from the right-click menu or the new Blacklist
button next to Add to Wishlist on the detail page. They are stored in a new
blacklist_entries table, so they stay hidden across restarts, and
Settings > Blacklist lists them with a Remove button.

The exclusion lives in the shared catalog WHERE clause on both the index and
the union query paths (electron/db/blacklistSql.js), so the Browse total and
scrollbar stay correct. Installed titles are never hidden, and blacklisting a
wishlisted title removes it from the wishlist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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