Public Access
feat(library): hide and unhide games
This commit is contained in:
1 parent
42ed0afe51
commit
011711fe4f
9 files changed
+180
-14
No files matched your search
+28
-11
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user