fix(statistics): keep player stats tab visible without local index (#851)

* fix(statistics): keep player stats tab visible without local index

Show the tab always and explain that player statistics require the local
library index, with a link to library settings, instead of hiding the tab.

* docs(release): CHANGELOG and credits for player stats tab UX (PR #851)

* docs(credits): drop minor library index and player stats tab fixes

Per team policy, small UX fixes (PR #850, #851) stay in CHANGELOG only.
This commit is contained in:
cucadmuh
2026-05-22 14:41:04 +03:00
committed by GitHub
parent 589afe9b7e
commit d54eceaf3b
15 changed files with 73 additions and 12 deletions
@@ -9,7 +9,9 @@ import {
type PlaySessionYearSummary,
} from '../../api/library';
import { usePlayerStatsLiveRefresh } from '../../hooks/usePlayerStatsLiveRefresh';
import { usePlayerStatsRecordingEnabled } from '../../hooks/usePlayerStatsRecordingEnabled';
import PlayerStatsHeatmap from './PlayerStatsHeatmap';
import PlayerStatsIndexRequiredNotice from './PlayerStatsIndexRequiredNotice';
import PlayerStatsPartialIndexNotice from './PlayerStatsPartialIndexNotice';
import PlayerStatsRecentDays from './PlayerStatsRecentDays';
import { formatPlayerStatsListeningTotal } from '../../utils/format/formatHumanDuration';
@@ -18,6 +20,7 @@ const currentCalendarYear = () => new Date().getFullYear();
export default function PlayerStatisticsPanel() {
const { t } = useTranslation();
const recordingEnabled = usePlayerStatsRecordingEnabled();
const [year, setYear] = useState(currentCalendarYear);
const [yearBounds, setYearBounds] = useState<PlaySessionYearBounds | null>(null);
const [summary, setSummary] = useState<PlaySessionYearSummary | null>(null);
@@ -28,6 +31,13 @@ export default function PlayerStatisticsPanel() {
const [liveRefreshKey, setLiveRefreshKey] = useState(0);
useEffect(() => {
if (!recordingEnabled) {
setLoading(false);
setSummary(null);
setDayCounts(new Map());
setSelectedDate(null);
return;
}
let cancelled = false;
setLoading(true);
setSelectedDate(null);
@@ -52,9 +62,10 @@ export default function PlayerStatisticsPanel() {
}
});
return () => { cancelled = true; };
}, [year]);
}, [year, recordingEnabled]);
const refreshLive = useCallback(async () => {
if (!recordingEnabled) return;
try {
const [s, heat] = await Promise.all([
libraryGetPlayerStatsYearSummary(year),
@@ -66,10 +77,18 @@ export default function PlayerStatisticsPanel() {
} catch {
/* ignore transient read errors during live refresh */
}
}, [year]);
}, [year, recordingEnabled]);
usePlayerStatsLiveRefresh(refreshLive);
if (!recordingEnabled) {
return (
<div className="stats-page">
<PlayerStatsIndexRequiredNotice />
</div>
);
}
const empty = !loading && (summary?.trackPlayCount ?? 0) === 0;
const calYear = currentCalendarYear();
const maxNavYear = yearBounds?.maxYear != null
@@ -0,0 +1,25 @@
import { Info } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
export default function PlayerStatsIndexRequiredNotice() {
const { t } = useTranslation();
const navigate = useNavigate();
return (
<div className="settings-hint settings-hint-info player-stats-partial-index-notice" role="status">
<Info size={16} aria-hidden style={{ flexShrink: 0, marginTop: 2 }} />
<span>
{t('statistics.playerIndexRequired')}
{' '}
<button
type="button"
className="player-stats-partial-index-link"
onClick={() => navigate('/settings', { state: { tab: 'library' } })}
>
{t('statistics.playerPartialIndexSettings')}
</button>
</span>
</div>
);
}
@@ -1,19 +1,10 @@
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom';
import { useAuthStore } from '../../store/authStore';
import { useLibraryIndexStore } from '../../store/libraryIndexStore';
export default function StatisticsTabBar() {
const { t } = useTranslation();
const navigate = useNavigate();
const location = useLocation();
const servers = useAuthStore(s => s.servers);
const masterEnabled = useLibraryIndexStore(s => s.masterEnabled);
const syncExcludedByServer = useLibraryIndexStore(s => s.syncExcludedByServer);
const showPlayerTab =
masterEnabled && servers.some(s => syncExcludedByServer[s.id] !== true);
if (!showPlayerTab) return null;
const isPlayer = location.pathname === '/player-stats';