Public Access
feat(library): add platform badges, remove hide duplicates
Gitea #34 slice 2 of 3. Core lists each set once now. The hide duplicates toggle and its param are not needed. PlatformIcons shows a badge per platform (platforms[]).
This commit is contained in:
1 parent
25cc5ff2d8
commit
16651a1f56
13 files changed
+120
-48
No files matched your search
+11
-18
@@ -23,6 +23,7 @@ import LibraryTabs from '../components/LibraryTabs.jsx';
|
||||
import ComingBadge from '../components/ComingBadge.jsx';
|
||||
import GameStatusBadge from '../components/GameStatusBadge.jsx';
|
||||
import PageContainer from '../components/PageContainer.jsx';
|
||||
import PlatformIcons from '../components/PlatformIcons.jsx';
|
||||
import {usePreferences} from '../context/PreferencesContext.jsx';
|
||||
import {coverEffect} from '../utils/rewards.js';
|
||||
import {apiFetch, request} from '../utils/api.js';
|
||||
@@ -58,15 +59,13 @@ export default function GameLibrary() {
|
||||
const q = searchParams.get('q') ?? '';
|
||||
const shortOnly = searchParams.get('maxMinutes') === String(SHORT_GAME_MINUTES);
|
||||
const hiddenOnly = searchParams.get('hidden') === 'true';
|
||||
const hideDuplicates = searchParams.get('hideDuplicates') === 'true';
|
||||
// The hidden list stands alone, also for a hand-made URL: its status and short filters are ignored.
|
||||
const listStatus = hiddenOnly ? null : status;
|
||||
const listShortOnly = !hiddenOnly && shortOnly;
|
||||
const listHideDuplicates = !hiddenOnly && hideDuplicates;
|
||||
const sources = parseSources(searchParams.get('source'));
|
||||
const listSources = hiddenOnly ? [] : sources;
|
||||
const sourceKey = listSources.join(',');
|
||||
const filtered = Boolean(listStatus || q.trim() || listShortOnly || hiddenOnly || listHideDuplicates || sourceKey);
|
||||
const filtered = Boolean(listStatus || q.trim() || listShortOnly || hiddenOnly || sourceKey);
|
||||
const [search, setSearch] = useState(q);
|
||||
const [adding, setAdding] = useState(false);
|
||||
// A Steam ID or a PlayStation link. Starts true until /games-count answers, so the sync controls do not vanish
|
||||
@@ -83,7 +82,7 @@ export default function GameLibrary() {
|
||||
const signal = nextSignal();
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const url = `${import.meta.env.VITE_CORE_API_URL}/api/library/games?${libraryQuery({page, size, sort, status: listStatus, q, maxMinutes: listShortOnly ? SHORT_GAME_MINUTES : null, hidden: hiddenOnly, hideDuplicates: listHideDuplicates, source: sourceKey})}`;
|
||||
const url = `${import.meta.env.VITE_CORE_API_URL}/api/library/games?${libraryQuery({page, size, sort, status: listStatus, q, maxMinutes: listShortOnly ? SHORT_GAME_MINUTES : null, hidden: hiddenOnly, source: sourceKey})}`;
|
||||
const {ok, status, data: json} = await request(url, {signal});
|
||||
if (signal.aborted) return;
|
||||
setIsLoading(false);
|
||||
@@ -93,7 +92,7 @@ export default function GameLibrary() {
|
||||
return;
|
||||
}
|
||||
setData(json);
|
||||
}, [nextSignal, page, size, sort, listStatus, q, listShortOnly, hiddenOnly, listHideDuplicates, sourceKey]);
|
||||
}, [nextSignal, page, size, sort, listStatus, q, listShortOnly, hiddenOnly, sourceKey]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
@@ -158,7 +157,7 @@ export default function GameLibrary() {
|
||||
};
|
||||
|
||||
const onHiddenChange = () => {
|
||||
updateParams(hiddenOnly ? {hidden: null, page: 0} : {hidden: true, status: null, maxMinutes: null, hideDuplicates: null, source: null, page: 0});
|
||||
updateParams(hiddenOnly ? {hidden: null, page: 0} : {hidden: true, status: null, maxMinutes: null, source: null, page: 0});
|
||||
};
|
||||
|
||||
const onSizeChange = (value) => {
|
||||
@@ -261,15 +260,6 @@ export default function GameLibrary() {
|
||||
>
|
||||
<Timer className="size-3"/>{t('LIBRARY.FILTER_SHORT')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={listHideDuplicates ? 'default' : 'outline'}
|
||||
aria-pressed={listHideDuplicates}
|
||||
className="h-7 rounded-full px-3 text-xs"
|
||||
onClick={() => updateParams({hideDuplicates: listHideDuplicates ? null : true, hidden: null, page: 0})}
|
||||
>
|
||||
<Layers className="size-3"/>{t('LIBRARY.FILTER_HIDE_DUPLICATES')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={hiddenOnly ? 'default' : 'outline'}
|
||||
@@ -346,10 +336,13 @@ export default function GameLibrary() {
|
||||
>
|
||||
<GameCover appid={g.appid} imageUrl={g.imageUrl} fx={coverEffect(equipped, g.status)}
|
||||
className="w-16 sm:w-24 shrink-0"/>
|
||||
<span className="@container min-w-0 flex-1 font-medium group-hover:text-primary transition-colors">
|
||||
<FitText text={g.name ?? `App ${g.appid}`}/>
|
||||
<span className="@container min-w-0 flex-1">
|
||||
<span className="block font-medium group-hover:text-primary transition-colors">
|
||||
<FitText text={g.name ?? `App ${g.appid}`}/>
|
||||
</span>
|
||||
<PlatformIcons platforms={g.platforms}/>
|
||||
</span>
|
||||
{platformLabel(g) && (
|
||||
{!g.platforms?.length && platformLabel(g) && (
|
||||
<Badge variant="outline" className="hidden sm:inline-flex" title={platformLabel(g)}>
|
||||
<Gamepad2 className="size-3"/>{platformLabel(g)}
|
||||
</Badge>
|
||||
|
||||
@@ -217,15 +217,22 @@ describe('GameLibrary short games', () => {
|
||||
expect(screen.getAllByText(/^x\d/)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('hideDuplicatesFilter_AddsHideDuplicatesToTheUrlAndTheRequest', async () => {
|
||||
const lib = renderLibrary();
|
||||
it('hasNoHideDuplicatesToggle_AndTheRequestNeverCarriesTheParam', async () => {
|
||||
renderLibrary('/library?hideDuplicates=true');
|
||||
await waitFor(() => expect(fetch).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByRole('button', {name: 'LIBRARY.FILTER_HIDE_DUPLICATES'}));
|
||||
expect(screen.queryByRole('button', {name: 'LIBRARY.FILTER_HIDE_DUPLICATES'})).toBeNull();
|
||||
expect(fetch.mock.calls.every(([url]) => !String(url).includes('hideDuplicates'))).toBe(true);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(lib.params().get('hideDuplicates')).toBe('true'));
|
||||
await waitFor(() => expect(fetch.mock.calls.at(-1)[0]).toMatch(/[?&]hideDuplicates=true(&|$)/));
|
||||
expect(screen.getByRole('button', {name: 'LIBRARY.FILTER_HIDE_DUPLICATES'}).getAttribute('aria-pressed')).toBe('true');
|
||||
it('showsThePlatformsOfAnEntryInsteadOfTheSinglePlatformBadge', async () => {
|
||||
answer([{appid: 220, name: 'Half-Life 2', playtimeMinutes: 0, status: 'BACKLOG', source: 'STEAM', platform: null,
|
||||
platforms: [{source: 'STEAM', platform: null}, {source: 'PSN', platform: 'PS5'}]}], true);
|
||||
renderLibrary();
|
||||
|
||||
expect(await screen.findByLabelText('PS5')).toBeTruthy();
|
||||
expect(screen.getByLabelText('Steam')).toBeTruthy();
|
||||
expect(screen.queryByText('Steam')).toBeNull();
|
||||
});
|
||||
|
||||
it('leaves the hidden games with a status click', async () => {
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('StatsPage', () => {
|
||||
|
||||
const link = await screen.findByRole('link', {name: /BACKLOG\.STATUS\.BACKLOG/});
|
||||
|
||||
expect(link.getAttribute('href')).toBe('/library?hideDuplicates=true&status=BACKLOG');
|
||||
expect(link.getAttribute('href')).toBe('/library?status=BACKLOG');
|
||||
});
|
||||
|
||||
it('aZeroTileIsNotClickable', async () => {
|
||||
@@ -178,7 +178,7 @@ describe('StatsPage', () => {
|
||||
const link = await within(await screen.findByText('STATS.OVERVIEW.TITLE').then(overview))
|
||||
.findByRole('link', {name: 'STATS.OVERVIEW.GAMES: 12'});
|
||||
|
||||
expect(link.getAttribute('href')).toBe('/library?hideDuplicates=true');
|
||||
expect(link.getAttribute('href')).toBe('/library');
|
||||
});
|
||||
|
||||
it('linksALegendRowToTheStatusLibrary', async () => {
|
||||
@@ -188,7 +188,7 @@ describe('StatsPage', () => {
|
||||
await screen.findByText('STATS.OVERVIEW.TITLE');
|
||||
|
||||
expect(within(overview()).getByRole('link', {name: /BACKLOG\.STATUS\.BACKLOG/}).getAttribute('href'))
|
||||
.toBe('/library?hideDuplicates=true&status=BACKLOG');
|
||||
.toBe('/library?status=BACKLOG');
|
||||
});
|
||||
|
||||
it('leavesAStatusWithoutGamesWithoutSliceAndTarget', async () => {
|
||||
@@ -211,7 +211,7 @@ describe('StatsPage', () => {
|
||||
fireEvent.click(paths()[1]);
|
||||
|
||||
await waitFor(() => expect(router.state.location.pathname + router.state.location.search)
|
||||
.toBe('/library?hideDuplicates=true&status=PLAYING'));
|
||||
.toBe('/library?status=PLAYING'));
|
||||
});
|
||||
|
||||
it('showsTheGamesTileInsteadOfTheDonutForAnEmptyLibrary', async () => {
|
||||
|
||||
Reference in new issue
Block a user