feat(library): hide and unhide games
Build, Test & Push Frontend / quality-check (push) Successful in 16s
Build, Test & Push Frontend / docker (push) Successful in 22s

This commit is contained in:
lobo committed 2026-09-26 12:27:58 +02:00
1 parent 42ed0afe51
commit 011711fe4f
9 files changed
+180 -14

No files matched your search

+28 -11
View File
@@ -7,7 +7,7 @@ import useForceSync from '../hooks/useForceSync.js';
import useSessionExpired from '../hooks/useSessionExpired.js';
import {formatPlaytime} from '../utils/games.js';
import {GAME_STATUSES, libraryQuery, parseStatus} from '../utils/backlog.js';
import {ArrowUpDown, ChevronLeft, ChevronRight, Clock, Gamepad2, Library, Plus, RefreshCw, Search, SearchX, Timer, Trophy} from 'lucide-react';
import {ArrowUpDown, ChevronLeft, ChevronRight, Clock, EyeOff, Gamepad2, Library, Plus, RefreshCw, Search, SearchX, Timer, Trophy} from 'lucide-react';
import {Badge} from '@/components/ui/badge';
import {Button} from '@/components/ui/button';
@@ -52,7 +52,11 @@ export default function GameLibrary() {
const status = parseStatus(searchParams.get('status'));
const q = searchParams.get('q') ?? '';
const shortOnly = searchParams.get('maxMinutes') === String(SHORT_GAME_MINUTES);
const filtered = Boolean(status || q.trim() || shortOnly);
const hiddenOnly = searchParams.get('hidden') === '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 filtered = Boolean(listStatus || q.trim() || listShortOnly || hiddenOnly);
const librarySearch = searchParams.toString();
const [search, setSearch] = useState(q);
const [adding, setAdding] = useState(false);
@@ -71,7 +75,7 @@ export default function GameLibrary() {
setIsLoading(true);
setError(null);
try {
const url = `${import.meta.env.VITE_CORE_API_URL}/api/library/games?${libraryQuery({page, size, sort, status, q, maxMinutes: shortOnly ? SHORT_GAME_MINUTES : 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})}`;
const response = await apiFetch(url, {
headers: {
'Content-Type': 'application/json',
@@ -96,7 +100,7 @@ export default function GameLibrary() {
} finally {
setIsLoading(false);
}
}, [navigate, expireSession, page, size, sort, status, q, shortOnly, t]);
}, [navigate, expireSession, page, size, sort, listStatus, q, listShortOnly, hiddenOnly, t]);
useEffect(() => {
load();
@@ -143,7 +147,11 @@ export default function GameLibrary() {
}, [search, q, searchParams]);
const onStatusChange = (value) => {
updateParams({status: value, page: 0});
updateParams({status: value, hidden: null, page: 0});
};
const onHiddenChange = () => {
updateParams(hiddenOnly ? {hidden: null, page: 0} : {hidden: true, status: null, maxMinutes: null, page: 0});
};
const onSizeChange = (value) => {
@@ -222,8 +230,8 @@ export default function GameLibrary() {
<Button
key={s ?? 'ALL'}
size="sm"
variant={status === s ? 'default' : 'outline'}
aria-pressed={status === s}
variant={listStatus === s && !hiddenOnly ? 'default' : 'outline'}
aria-pressed={listStatus === s && !hiddenOnly}
className="h-7 rounded-full px-3 text-xs"
onClick={() => onStatusChange(s)}
>
@@ -232,13 +240,22 @@ export default function GameLibrary() {
))}
<Button
size="sm"
variant={shortOnly ? 'default' : 'outline'}
aria-pressed={shortOnly}
variant={listShortOnly ? 'default' : 'outline'}
aria-pressed={listShortOnly}
className="h-7 rounded-full px-3 text-xs"
onClick={() => updateParams({maxMinutes: shortOnly ? null : SHORT_GAME_MINUTES, page: 0})}
onClick={() => updateParams({maxMinutes: listShortOnly ? null : SHORT_GAME_MINUTES, hidden: null, page: 0})}
>
<Timer className="size-3"/>{t('LIBRARY.FILTER_SHORT')}
</Button>
<Button
size="sm"
variant={hiddenOnly ? 'default' : 'outline'}
aria-pressed={hiddenOnly}
className="h-7 rounded-full px-3 text-xs"
onClick={onHiddenChange}
>
<EyeOff className="size-3"/>{t('LIBRARY.FILTER_HIDDEN')}
</Button>
</div>
<div className="relative sm:w-64">
<Search className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"/>
@@ -272,7 +289,7 @@ export default function GameLibrary() {
<Card className="items-center py-16 text-center text-muted-foreground">
{filtered ? <SearchX className="size-10"/> : <Library className="size-10"/>}
<p className="max-w-sm text-sm">
{filtered ? t('LIBRARY.NO_MATCH')
{hiddenOnly ? t('LIBRARY.NO_HIDDEN') : filtered ? t('LIBRARY.NO_MATCH')
: sync.isRunning ? t('LIBRARY.SYNCING_FIRST_TIME') : t('LIBRARY.EMPTY')}
</p>
</Card>