feat(library): show set total playtime on dashboard cards

Most played and currently playing cards show the total playtime of
the game set. Use the own minutes when the API sends no total.
This commit is contained in:
lobo committed 2026-10-09 14:35:07 +02:00
1 parent f1a6bc0443
commit 9adc606c57
5 files changed
+44 -6

No files matched your search

+1 -1
View File
@@ -68,7 +68,7 @@ To Be Played is split into three repositories:
- **Detail per platform**: the details page of a game with several platforms (or a PlayStation title on a Steam game)
shows `PlatformsCard` above the editions: one row per platform with playtime, last played and the achievement or
trophy progress of that platform, a total row, and a link to the edition whose page lists the achievements. The hero
playtime and the library card show the set total (`totalPlaytimeMinutes`); sorting and the stats stay per edition.
playtime and the library card show the set total (`totalPlaytimeMinutes`); sorting, stats, dashboard cards and the public profile use the set total (Core V45); per row stay the playtime history, rewards and the played / never started counts.
- **Add a game by name** for games outside Steam (PlayStation, Switch, Xbox, physical): search IGDB, pick a hit,
a platform and optional hours. Works without a Steam ID. Games already in ToBePlayed come first. Check several
hits to add them at once through the import review.
+1 -1
View File
@@ -40,7 +40,7 @@ export default function CurrentlyPlayingCard({games, syncing, wide}) {
<FitText text={g.name ?? `App ${g.appid}`}/>
</div>
<div className="text-xs text-muted-foreground tabular-nums">
{formatPlaytime(g.playtimeMinutes)}
{formatPlaytime(g.totalPlaytimeMinutes ?? g.playtimeMinutes)}
</div>
</Link>
</li>
@@ -0,0 +1,18 @@
// @vitest-environment jsdom
import {afterEach, describe, expect, it, vi} from 'vitest';
import {cleanup, render, screen} from '@testing-library/react';
import {MemoryRouter} from 'react-router-dom';
import CurrentlyPlayingCard from './CurrentlyPlayingCard.jsx';
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
describe('CurrentlyPlayingCard', () => {
afterEach(cleanup);
it('shows the set total of a game, not the minutes of its own row', () => {
render(<MemoryRouter><CurrentlyPlayingCard games={[{appid: 620, name: 'Portal 2', playtimeMinutes: 100,
totalPlaytimeMinutes: 600, status: 'PLAYING'}]}/></MemoryRouter>);
expect(screen.getByText('10h')).toBeTruthy();
});
});
+6 -4
View File
@@ -12,15 +12,17 @@ import {formatPlaytime} from '../utils/games.js';
import {coverEffect} from '../utils/rewards.js';
import {playChart} from '../utils/statCharts.js';
const total = (g) => g.totalPlaytimeMinutes ?? g.playtimeMinutes;
/**
* @param chart LIST, BARS, PIE or DONUT
* @param onChart `(form)`: shows the picker
*/
export default function MostPlayedCard({games, syncing, equipped, chart = 'LIST', onChart}) {
const {t} = useTranslation();
const mapped = games.map((g) => ({appid: g.appid, name: g.name, minutes: g.playtimeMinutes}));
const mapped = games.map((g) => ({appid: g.appid, name: g.name, minutes: total(g)}));
const form = playChart(chart, mapped);
const maxMinutes = games[0]?.playtimeMinutes || 1;
const maxMinutes = mapped[0]?.minutes || 1;
return (
<Card className="gap-0 py-0">
<div className="flex flex-wrap items-center justify-between gap-x-3 px-5 py-4 border-b">
@@ -62,12 +64,12 @@ export default function MostPlayedCard({games, syncing, equipped, chart = 'LIST'
<div className="mt-2 h-1.5 rounded-full bg-muted">
<div
className="h-full rounded-full bg-primary"
style={{width: `${Math.max(2, (g.playtimeMinutes / maxMinutes) * 100)}%`}}
style={{width: `${Math.max(2, (total(g) / maxMinutes) * 100)}%`}}
/>
</div>
</div>
<span className="w-12 sm:w-14 text-right text-sm font-medium text-muted-foreground tabular-nums">
{formatPlaytime(g.playtimeMinutes)}
{formatPlaytime(total(g))}
</span>
</Link>
</li>
+18
View File
@@ -0,0 +1,18 @@
// @vitest-environment jsdom
import {afterEach, describe, expect, it, vi} from 'vitest';
import {cleanup, render, screen} from '@testing-library/react';
import {MemoryRouter} from 'react-router-dom';
import MostPlayedCard from './MostPlayedCard.jsx';
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
describe('MostPlayedCard', () => {
afterEach(cleanup);
it('shows the set total of a game, not the minutes of its own row', () => {
render(<MemoryRouter><MostPlayedCard games={[{appid: 620, name: 'Portal 2', playtimeMinutes: 100,
totalPlaytimeMinutes: 600, status: 'PLAYING'}]}/></MemoryRouter>);
expect(screen.getByText('10h')).toBeTruthy();
});
});