Public Access
The sync details show a Set to 100% button for ALL_UNLOCKED games. The button uses the existing status PUT, checks points, and reloads the Dashboard. Refs #21
75 lines
3.5 KiB
React
75 lines
3.5 KiB
React
import {useEffect, useState} from 'react';
|
|
import {useTranslation} from 'react-i18next';
|
|
|
|
import {Dialog, DialogContent, DialogDescription, DialogTitle} from '@/components/ui/dialog';
|
|
import {Skeleton} from '@/components/ui/skeleton';
|
|
import SyncCompletedButton from './SyncCompletedButton.jsx';
|
|
import GameRow from './stats/GameRow.jsx';
|
|
import {formatPlaytime} from '../utils/games.js';
|
|
import {fetchSyncChanges, groupByKind} from '../utils/syncChanges.js';
|
|
|
|
/**
|
|
* The changes of the last sync, grouped by kind. Loads the result again when it opens: the achievements of a sync
|
|
* arrive after its games.
|
|
*
|
|
* @param onChanged called after a game was set to COMPLETED from here
|
|
*/
|
|
export default function SyncChangesDialog({onClose, onChanged}) {
|
|
const {t} = useTranslation();
|
|
const [changes, setChanges] = useState(null);
|
|
const [failed, setFailed] = useState(false);
|
|
|
|
useEffect(() => {
|
|
let current = true;
|
|
fetchSyncChanges()
|
|
.then((body) => {
|
|
if (!current) return;
|
|
if (body) setChanges(body.changes ?? []);
|
|
else setFailed(true);
|
|
})
|
|
.catch(() => current && setFailed(true));
|
|
return () => {
|
|
current = false;
|
|
};
|
|
}, []);
|
|
|
|
const value = (change) => {
|
|
switch (change.kind) {
|
|
case 'PLAYTIME': return `+${formatPlaytime(change.amount)}`;
|
|
case 'ACHIEVEMENTS': return `+${change.amount}`;
|
|
case 'STATUS': return `${t(`BACKLOG.STATUS.${change.fromStatus}`)} → ${t(`BACKLOG.STATUS.${change.toStatus}`)}`;
|
|
default: return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
|
<DialogContent
|
|
className="max-h-[85vh] overflow-y-auto overflow-x-hidden max-sm:top-auto max-sm:bottom-0 max-sm:max-w-full max-sm:translate-y-0 max-sm:rounded-b-none sm:max-w-xl">
|
|
<DialogTitle className="pr-6">{t('SYNC_CHANGES.TITLE')}</DialogTitle>
|
|
<DialogDescription className="sr-only">{t('SYNC_CHANGES.TITLE')}</DialogDescription>
|
|
{changes === null && !failed && <Skeleton className="h-24 rounded-xl"/>}
|
|
{failed && <p className="text-sm text-muted-foreground">{t('SYNC_CHANGES.ERROR')}</p>}
|
|
{changes && changes.length === 0 && <p className="text-sm text-muted-foreground">{t('SYNC_CHANGES.EMPTY')}</p>}
|
|
{changes && groupByKind(changes).map(({kind, changes: rows}) => (
|
|
<section key={kind} className="space-y-2">
|
|
<h3 className="text-sm font-semibold">{t(`SYNC_CHANGES.HEADING.${kind}`)}</h3>
|
|
<ul className="space-y-2">
|
|
{rows.map((change, index) => kind === 'ALL_UNLOCKED' ? (
|
|
<li key={change.appid} className="flex items-center gap-3">
|
|
<div className="min-w-0 flex-1"><GameRow game={change} linked/></div>
|
|
<SyncCompletedButton appid={change.appid} onChanged={onChanged}/>
|
|
</li>
|
|
) : (
|
|
<li key={`${change.appid}-${change.fromStatus}-${index}`}>
|
|
<GameRow game={change} value={value(change)} linked/>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
))}
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|