import React, { useCallback, useEffect, useState } from 'react'; import { ChevronLeft, ChevronRight } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { libraryGetPlayerStatsHeatmap, libraryGetPlayerStatsYearBounds, libraryGetPlayerStatsYearSummary, type PlaySessionYearBounds, 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'; const currentCalendarYear = () => new Date().getFullYear(); export default function PlayerStatisticsPanel() { const { t } = useTranslation(); const recordingEnabled = usePlayerStatsRecordingEnabled(); const [year, setYear] = useState(currentCalendarYear); const [yearBounds, setYearBounds] = useState(null); const [summary, setSummary] = useState(null); const [dayCounts, setDayCounts] = useState>(new Map()); const [selectedDate, setSelectedDate] = useState(null); const [loading, setLoading] = useState(true); const [recentRefreshKey, setRecentRefreshKey] = useState(0); 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); Promise.all([ libraryGetPlayerStatsYearSummary(year), libraryGetPlayerStatsHeatmap(year), libraryGetPlayerStatsYearBounds(), ]) .then(([s, heat, bounds]) => { if (cancelled) return; setSummary(s); setDayCounts(new Map(heat.map(h => [h.date, h.trackPlayCount]))); setYearBounds(bounds); setLoading(false); setRecentRefreshKey(k => k + 1); }) .catch(() => { if (!cancelled) { setSummary(null); setDayCounts(new Map()); setLoading(false); } }); return () => { cancelled = true; }; }, [year, recordingEnabled]); const refreshLive = useCallback(async () => { if (!recordingEnabled) return; try { const [s, heat] = await Promise.all([ libraryGetPlayerStatsYearSummary(year), libraryGetPlayerStatsHeatmap(year), ]); setSummary(s); setDayCounts(new Map(heat.map(h => [h.date, h.trackPlayCount]))); setLiveRefreshKey(k => k + 1); } catch { /* ignore transient read errors during live refresh */ } }, [year, recordingEnabled]); usePlayerStatsLiveRefresh(refreshLive); if (!recordingEnabled) { return (
); } const empty = !loading && (summary?.trackPlayCount ?? 0) === 0; const calYear = currentCalendarYear(); const maxNavYear = yearBounds?.maxYear != null ? Math.min(calYear, yearBounds.maxYear) : calYear; const canGoPrev = !loading && yearBounds?.minYear != null && year > yearBounds.minYear; const canGoNext = !loading && year < maxNavYear; return (
{year}
{loading ? (
) : ( <> {!empty && summary && (
{formatPlayerStatsListeningTotal(summary.totalListenedSec)} {t('statistics.playerSummaryTime')}
{summary.sessionCount.toLocaleString()} {t('statistics.playerSummarySessions')}
{summary.trackPlayCount.toLocaleString()} {t('statistics.playerSummaryTracks')}
{summary.uniqueTrackCount.toLocaleString()} {t('statistics.playerSummaryUniqueTracks')}
{summary.listeningDayCount.toLocaleString()} {t('statistics.playerSummaryDays')}
{summary.fullCount} / {summary.partialCount} {t('statistics.playerSummaryCompletion')}
)} {empty && (

{t('statistics.playerEmpty')}

)} )} {!loading && ( )}
); }