diff --git a/README.md b/README.md
index 3a4165c..3f9ccd0 100644
--- a/README.md
+++ b/README.md
@@ -55,12 +55,14 @@ To Be Played is split into three repositories:
system setting "reduce motion" or the "Reduce animations" switch in the settings.
- **Landing page**: next to the community numbers and the top games, three blocks: games per platform, top genres and
finished games per year of all users.
-- **Library**: status filters, source filter (Steam, PlayStation, manual; a linked set counts on each of its sources), "short games" filter (up to 10 h), search, sorting by playtime, recently seen, newest
- in library, rating or priority, paging. Each row shows the unlocked achievements.
+- **Library**: status filters, source filter (Steam, PlayStation, manual; a linked set matches on each of its sources), "short games" filter (up to 10 h), search, sorting by playtime, recently seen, newest
+ in library, rating or priority, paging. Each row shows the unlocked achievements. A game you own on several
+ platforms is one row (Core's counted edition) with a small badge per platform next to the title (`PlatformIcons`:
+ ST, PS or the platform name, from `platforms[]`; lucide has no brand icons).
- **Editions**: the editions of one game (Steam and PS5 copy, remaster, port) are linked automatically from IGDB's
"same game" key; an auditor can also link or unlink them for every user. A user who owns two or more of a linked
- set sees "Also in your library" on the details page, an "x2" badge in the library and the filter "Hide
- duplicates"; the numbers count the set once. The editions of a set share one status, rating and notes (the page
+ set sees "Also in your library" on the details page and an "x2" badge in the library; the library lists the set
+ once (no "Hide duplicates" filter any more), and the numbers count it once. The editions of a set share one status, rating and notes (the page
reloads after a save to show it). "Not the same game" splits the game off its set for this user only, and it stays
split. "Same game as..." on the details page asks the auditors for a link.
- **Add a game by name** for games outside Steam (PlayStation, Switch, Xbox, physical): search IGDB, pick a hit,
@@ -259,6 +261,13 @@ flowchart LR
which question is open; `src/utils/questions.js` only loads the list and sends the answer. A yes to Finished? gets
the Rate it? card back at once. `409` / `404` reload the list. `RatingStars` is shared with `BacklogCard`.
+```mermaid
+flowchart LR
+ Games["Core GET /api/library/games: one row per set + platforms[]"] --> Row["GameLibrary row"]
+ Row --> Icons["PlatformIcons: badge per platform"]
+ Row -.->|"no platforms (hidden game)"| Badge["single platform badge"]
+```
+
```mermaid
flowchart LR
Detail["GameDetail: EditionsCard"] -->|"Not the same game"| Split["Core DELETE /api/library/games/{appid}/set"]
diff --git a/public/i18n/de.json b/public/i18n/de.json
index e6f5804..a54e575 100644
--- a/public/i18n/de.json
+++ b/public/i18n/de.json
@@ -291,7 +291,6 @@
"FILTER_HIDDEN": "Ausgeblendet",
"NO_HIDDEN": "Keine ausgeblendeten Spiele. Blende ein Spiel auf seiner Detailseite aus, z. B. einen Playtest oder ein Tool.",
"ACHIEVEMENTS": "{{unlocked}} von {{total}} Erfolgen",
- "FILTER_HIDE_DUPLICATES": "Duplikate ausblenden",
"EDITIONS": "{{count}} Ausgaben in deiner Bibliothek"
},
"GAME_DETAIL": {
diff --git a/public/i18n/en.json b/public/i18n/en.json
index a78e0d0..177de84 100644
--- a/public/i18n/en.json
+++ b/public/i18n/en.json
@@ -291,7 +291,6 @@
"FILTER_HIDDEN": "Hidden",
"NO_HIDDEN": "No hidden games. Hide a game on its detail page, e.g. a playtest or a tool.",
"ACHIEVEMENTS": "{{unlocked}} of {{total}} achievements",
- "FILTER_HIDE_DUPLICATES": "Hide duplicates",
"EDITIONS": "{{count}} editions in your library"
},
"GAME_DETAIL": {
diff --git a/src/components/PileOfShameCard.test.jsx b/src/components/PileOfShameCard.test.jsx
index 9b37952..e2be322 100644
--- a/src/components/PileOfShameCard.test.jsx
+++ b/src/components/PileOfShameCard.test.jsx
@@ -116,7 +116,7 @@ describe('PileOfShameCard', () => {
render( {}}/>);
expect(screen.getAllByRole('link', {name: 'BACKLOG.STATUS.BACKLOG: 2'})[0].getAttribute('href'))
- .toBe('/library?hideDuplicates=true&status=BACKLOG');
+ .toBe('/library?status=BACKLOG');
fireEvent.click(screen.getByRole('button', {name: 'DASHBOARD.NEVER_PLAYED:1'}));
expect(await screen.findByText('Untouched')).toBeTruthy();
diff --git a/src/components/PlatformIcons.jsx b/src/components/PlatformIcons.jsx
new file mode 100644
index 0000000..9652ce2
--- /dev/null
+++ b/src/components/PlatformIcons.jsx
@@ -0,0 +1,30 @@
+import {useTranslation} from 'react-i18next';
+
+import {platformLabel} from '../utils/platforms.js';
+
+// lucide has no brand icons: a short text badge per platform.
+const shortName = ({source, platform}) => {
+ if (source === 'STEAM') return 'ST';
+ return platform ?? (source === 'PSN' ? 'PS' : 'M');
+};
+
+/**
+ * @param platforms the {source, platform} of every edition of a library entry, see Core's `platforms`
+ */
+export default function PlatformIcons({platforms}) {
+ const {t} = useTranslation();
+ if (!platforms?.length) return null;
+ return (
+