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
@@ -1,7 +1,8 @@
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
import {Link, useNavigate, useParams} from 'react-router-dom';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import {ArrowLeft, Clock, ExternalLink, Gamepad2, Image as ImageIcon, ImageOff, PanelTop, RotateCcw, ShieldOff, Timer, Trophy} from 'lucide-react';
|
||||
import {ArrowLeft, Clock, ExternalLink, Eye, EyeOff, Gamepad2, Image as ImageIcon, ImageOff, PanelTop, RotateCcw, ShieldOff, Timer, Trophy} from 'lucide-react';
|
||||
import {toast} from 'sonner';
|
||||
|
||||
import {Badge} from '@/components/ui/badge';
|
||||
import {Button} from '@/components/ui/button';
|
||||
@@ -22,6 +23,7 @@ import {usePreferences} from '../context/PreferencesContext.jsx';
|
||||
import PageContainer from '../components/PageContainer.jsx';
|
||||
import {apiFetch} from '../utils/api.js';
|
||||
import {ACHIEVEMENT_ICONS, reportBrokenImage} from '../utils/brokenImages.js';
|
||||
import {setGameHidden} from '../utils/hiddenGames.js';
|
||||
import {hasSession} from '../utils/auth.js';
|
||||
import {isSteamKey} from '../utils/manualGames.js';
|
||||
|
||||
@@ -58,6 +60,7 @@ export default function GameDetail() {
|
||||
|
||||
const [data, setData] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hiding, setHiding] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -124,6 +127,29 @@ export default function GameDetail() {
|
||||
const isBackground = !!data && appearance.backgroundAppid === data.appid;
|
||||
const isBanner = !!data && appearance.showBanner && appearance.bannerAppid === data.appid;
|
||||
|
||||
const toggleHidden = async () => {
|
||||
const hidden = !data.hidden;
|
||||
setHiding(true);
|
||||
try {
|
||||
const response = await setGameHidden(data.appid, hidden);
|
||||
if (response.status === 401) {
|
||||
expireSession();
|
||||
return;
|
||||
}
|
||||
if (!response.ok) {
|
||||
toast.error(t('GAME_DETAIL.HIDE_ERROR'));
|
||||
return;
|
||||
}
|
||||
setData((d) => ({...d, hidden}));
|
||||
toast.success(t(hidden ? 'GAME_DETAIL.HIDDEN_TOAST' : 'GAME_DETAIL.SHOWN_TOAST'));
|
||||
} catch (err) {
|
||||
console.error('Hide game error:', err);
|
||||
toast.error(t('INFO.ERROR.NETWORK_FAILURE'));
|
||||
} finally {
|
||||
setHiding(false);
|
||||
}
|
||||
};
|
||||
|
||||
const stats = data ? [
|
||||
[t('GAME_DETAIL.UNLOCKED'), data.achievementsUnlocked],
|
||||
[t('GAME_DETAIL.REMAINING'), data.achievementsRemaining],
|
||||
@@ -152,6 +178,11 @@ export default function GameDetail() {
|
||||
|
||||
{!isLoading && !error && data && (
|
||||
<>
|
||||
{data.hidden && (
|
||||
<div role="status" className="flex items-center gap-2 rounded-lg border bg-muted/50 px-4 py-3 text-sm text-muted-foreground">
|
||||
<EyeOff className="size-4 shrink-0"/>{t('GAME_DETAIL.HIDDEN_NOTE')}
|
||||
</div>
|
||||
)}
|
||||
<Card className="relative overflow-hidden p-0 gap-0 md:flex-row">
|
||||
<GameCover appid={data.appid} imageUrl={data.imageUrl} className="w-full rounded-none md:w-96 md:shrink-0"/>
|
||||
<div className="flex flex-1 flex-col justify-between gap-4 p-6">
|
||||
@@ -180,7 +211,8 @@ export default function GameDetail() {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{/* Store page, background and banner need Steam art, so a game off Steam has none of them. */}
|
||||
{/* Store page, background and banner need Steam art, so a game off Steam has none of them.
|
||||
Hide is here too: only a Steam game can be hidden. */}
|
||||
{onSteam && <div className="flex flex-wrap gap-2">
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<a href={`https://store.steampowered.com/app/${data.appid}`} target="_blank" rel="noopener noreferrer">
|
||||
@@ -206,6 +238,12 @@ export default function GameDetail() {
|
||||
<PanelTop/>{t('GAME_DETAIL.SET_BANNER')}
|
||||
</Button>
|
||||
)}
|
||||
{!manual && (
|
||||
<Button variant="outline" size="sm" onClick={toggleHidden} disabled={hiding}>
|
||||
{data.hidden ? <Eye/> : <EyeOff/>}
|
||||
{t(data.hidden ? 'GAME_DETAIL.UNHIDE' : 'GAME_DETAIL.HIDE')}
|
||||
</Button>
|
||||
)}
|
||||
</div>}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
Reference in new issue
Block a user