From d54eceaf3b4fe54f8e24f0bae5ac478e80561b3b Mon Sep 17 00:00:00 2001 From: cucadmuh <49571317+cucadmuh@users.noreply.github.com> Date: Fri, 22 May 2026 14:41:04 +0300 Subject: [PATCH] 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. --- CHANGELOG.md | 8 ++++++ .../statistics/PlayerStatisticsPanel.tsx | 23 +++++++++++++++-- .../PlayerStatsIndexRequiredNotice.tsx | 25 +++++++++++++++++++ .../statistics/StatisticsTabBar.tsx | 9 ------- src/config/settingsCredits.ts | 1 - src/hooks/usePlayerStatsRecordingEnabled.ts | 10 ++++++++ src/locales/de/statistics.ts | 1 + src/locales/en/statistics.ts | 1 + src/locales/es/statistics.ts | 1 + src/locales/fr/statistics.ts | 1 + src/locales/nb/statistics.ts | 1 + src/locales/nl/statistics.ts | 1 + src/locales/ro/statistics.ts | 1 + src/locales/ru/statistics.ts | 1 + src/locales/zh/statistics.ts | 1 + 15 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 src/components/statistics/PlayerStatsIndexRequiredNotice.tsx create mode 100644 src/hooks/usePlayerStatsRecordingEnabled.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 791494d2..8e3f0c18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -164,6 +164,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +### Statistics — player stats tab without local index + +**By [@cucadmuh](https://github.com/cucadmuh), PR [#851](https://github.com/Psychotoxical/psysonic/pull/851)** + +* **Statistics → Player stats** tab stays visible when the local library index is off; an info notice explains that player statistics require the index and links to **Settings → Library**. + + + ## [1.46.0] - 2026-05-18 > **🙏 Special thanks to [@zz5zz](https://github.com/zz5zz)** for his tireless quirk-spotting and bug reports on the [Psysonic Discord](https://discord.gg/AMnDRErm4u) — several of the polish fixes in this release landed directly off the back of his messages. diff --git a/src/components/statistics/PlayerStatisticsPanel.tsx b/src/components/statistics/PlayerStatisticsPanel.tsx index 6d19502f..3398b5c5 100644 --- a/src/components/statistics/PlayerStatisticsPanel.tsx +++ b/src/components/statistics/PlayerStatisticsPanel.tsx @@ -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(null); const [summary, setSummary] = useState(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 ( +
+ +
+ ); + } + const empty = !loading && (summary?.trackPlayCount ?? 0) === 0; const calYear = currentCalendarYear(); const maxNavYear = yearBounds?.maxYear != null diff --git a/src/components/statistics/PlayerStatsIndexRequiredNotice.tsx b/src/components/statistics/PlayerStatsIndexRequiredNotice.tsx new file mode 100644 index 00000000..8f8d9ab2 --- /dev/null +++ b/src/components/statistics/PlayerStatsIndexRequiredNotice.tsx @@ -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 ( +
+ + + {t('statistics.playerIndexRequired')} + {' '} + + +
+ ); +} diff --git a/src/components/statistics/StatisticsTabBar.tsx b/src/components/statistics/StatisticsTabBar.tsx index 0670ef90..9cc8d598 100644 --- a/src/components/statistics/StatisticsTabBar.tsx +++ b/src/components/statistics/StatisticsTabBar.tsx @@ -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'; diff --git a/src/config/settingsCredits.ts b/src/config/settingsCredits.ts index 72c22859..aa3d1855 100644 --- a/src/config/settingsCredits.ts +++ b/src/config/settingsCredits.ts @@ -125,7 +125,6 @@ const CONTRIBUTOR_ENTRIES = [ 'Local library index: multi-server settings UI, serial sync queue, music-library-scoped local search, parallel initial ingest, i18n across 9 locales (PR #846)', 'Library browse: local-vs-network text search race, All Albums/Artists catalog from index, DevTools browse-race logging (PR #847)', 'Player stats: local listening history tab with heatmap, year summary, recent days, and day drill-down (PR #849)', - 'Settings → Library: exclude/include index buttons show busy state and block repeat clicks (PR #850)', ], }, { diff --git a/src/hooks/usePlayerStatsRecordingEnabled.ts b/src/hooks/usePlayerStatsRecordingEnabled.ts new file mode 100644 index 00000000..aad5856f --- /dev/null +++ b/src/hooks/usePlayerStatsRecordingEnabled.ts @@ -0,0 +1,10 @@ +import { useAuthStore } from '../store/authStore'; +import { useLibraryIndexStore } from '../store/libraryIndexStore'; + +/** True when local play history is recorded (master index on + ≥1 server included). */ +export function usePlayerStatsRecordingEnabled(): boolean { + const servers = useAuthStore(s => s.servers); + const masterEnabled = useLibraryIndexStore(s => s.masterEnabled); + const syncExcludedByServer = useLibraryIndexStore(s => s.syncExcludedByServer); + return masterEnabled && servers.some(s => syncExcludedByServer[s.id] !== true); +} diff --git a/src/locales/de/statistics.ts b/src/locales/de/statistics.ts index 674f16ce..c617e51b 100644 --- a/src/locales/de/statistics.ts +++ b/src/locales/de/statistics.ts @@ -74,6 +74,7 @@ export const statistics = { playerSummaryUniqueTracks: 'Einzigartige Titel', playerSummaryDays: 'Days', playerPartialIndexNotice: 'Einige Server sind vom lokalen Bibliotheksindex ausgeschlossen. Wiedergaben auf diesen Servern werden nicht in der Player-Statistik erfasst.', + playerIndexRequired: 'Player-Statistiken sind erst verfügbar, wenn der lokale Bibliotheksindex aktiviert ist.', playerPartialIndexSettings: 'Bibliothekseinstellungen', playerSummaryCompletion: 'Voll / teilweise', playerYearPrev: 'Vorheriges Jahr', diff --git a/src/locales/en/statistics.ts b/src/locales/en/statistics.ts index f0eca0ec..d994c2e1 100644 --- a/src/locales/en/statistics.ts +++ b/src/locales/en/statistics.ts @@ -74,6 +74,7 @@ export const statistics = { playerSummaryUniqueTracks: 'Unique tracks', playerSummaryDays: 'Days', playerPartialIndexNotice: 'Some servers are excluded from the local library index. Listening on those servers is not recorded in player statistics.', + playerIndexRequired: 'Player statistics are not available until you enable the local library index.', playerPartialIndexSettings: 'Library settings', playerSummaryCompletion: 'Full / partial', playerYearPrev: 'Previous year', diff --git a/src/locales/es/statistics.ts b/src/locales/es/statistics.ts index 7a16c9f5..e2f01ccf 100644 --- a/src/locales/es/statistics.ts +++ b/src/locales/es/statistics.ts @@ -56,6 +56,7 @@ export const statistics = { playerSummaryUniqueTracks: 'Pistas únicas', playerSummaryDays: 'Days', playerPartialIndexNotice: 'Algunos servidores están excluidos del índice local de biblioteca. La escucha en esos servidores no se registra en las estadísticas del reproductor.', + playerIndexRequired: 'Las estadísticas del reproductor no están disponibles hasta que actives el índice local de biblioteca.', playerPartialIndexSettings: 'Ajustes de biblioteca', playerSummaryCompletion: 'Completas / parciales', playerYearPrev: 'Año anterior', diff --git a/src/locales/fr/statistics.ts b/src/locales/fr/statistics.ts index ceec04a4..7b212b5c 100644 --- a/src/locales/fr/statistics.ts +++ b/src/locales/fr/statistics.ts @@ -56,6 +56,7 @@ export const statistics = { playerSummaryUniqueTracks: 'Morceaux uniques', playerSummaryDays: 'Days', playerPartialIndexNotice: 'Certains serveurs sont exclus de l’index local de bibliothèque. L’écoute sur ces serveurs n’est pas enregistrée dans les statistiques du lecteur.', + playerIndexRequired: 'Les statistiques du lecteur ne sont pas disponibles tant que l’index local de bibliothèque n’est pas activé.', playerPartialIndexSettings: 'Paramètres bibliothèque', playerSummaryCompletion: 'Complets / partiels', playerYearPrev: 'Année précédente', diff --git a/src/locales/nb/statistics.ts b/src/locales/nb/statistics.ts index 5e82ed23..8621971a 100644 --- a/src/locales/nb/statistics.ts +++ b/src/locales/nb/statistics.ts @@ -56,6 +56,7 @@ export const statistics = { playerSummaryUniqueTracks: 'Unike spor', playerSummaryDays: 'Days', playerPartialIndexNotice: 'Noen servere er ekskludert fra det lokale biblioteksindeks. Lytting på disse serverne registreres ikke i spillerstatistikken.', + playerIndexRequired: 'Spillerstatistikk er ikke tilgjengelig før du aktiverer det lokale biblioteksindeks.', playerPartialIndexSettings: 'Bibliotekinnstillinger', playerSummaryCompletion: 'Full / delvis', playerYearPrev: 'Forrige år', diff --git a/src/locales/nl/statistics.ts b/src/locales/nl/statistics.ts index 79003803..d7a528c0 100644 --- a/src/locales/nl/statistics.ts +++ b/src/locales/nl/statistics.ts @@ -56,6 +56,7 @@ export const statistics = { playerSummaryUniqueTracks: 'Unieke nummers', playerSummaryDays: 'Days', playerPartialIndexNotice: 'Sommige servers zijn uitgesloten van de lokale bibliotheekindex. Luisteren op die servers wordt niet vastgelegd in de playerstatistieken.', + playerIndexRequired: 'Playerstatistieken zijn niet beschikbaar totdat je de lokale bibliotheekindex inschakelt.', playerPartialIndexSettings: 'Bibliotheekinstellingen', playerSummaryCompletion: 'Volledig / gedeeltelijk', playerYearPrev: 'Vorig jaar', diff --git a/src/locales/ro/statistics.ts b/src/locales/ro/statistics.ts index c344a1d1..49eacf5d 100644 --- a/src/locales/ro/statistics.ts +++ b/src/locales/ro/statistics.ts @@ -74,6 +74,7 @@ export const statistics = { playerSummaryUniqueTracks: 'Piese unice', playerSummaryDays: 'Days', playerPartialIndexNotice: 'Unele servere sunt excluse din indexul local al bibliotecii. Ascultarea pe aceste servere nu este înregistrată în statisticile playerului.', + playerIndexRequired: 'Statisticile playerului nu sunt disponibile până când activezi indexul local al bibliotecii.', playerPartialIndexSettings: 'Setări bibliotecă', playerSummaryCompletion: 'Complete / parțiale', playerYearPrev: 'Anul anterior', diff --git a/src/locales/ru/statistics.ts b/src/locales/ru/statistics.ts index 4855488d..c5d8e1ed 100644 --- a/src/locales/ru/statistics.ts +++ b/src/locales/ru/statistics.ts @@ -67,6 +67,7 @@ export const statistics = { playerSummaryUniqueTracks: 'Уникальные треки', playerSummaryDays: 'Days', playerPartialIndexNotice: 'Не все серверы включены в локальный индекс библиотеки. Прослушивание с выключенных серверов не попадает в эту статистику.', + playerIndexRequired: 'Статистика плеера недоступна, пока не включён локальный индекс библиотеки.', playerPartialIndexSettings: 'Настройки библиотеки', playerSummaryCompletion: 'Полные / частичные', playerYearPrev: 'Предыдущий год', diff --git a/src/locales/zh/statistics.ts b/src/locales/zh/statistics.ts index 170aa207..74665359 100644 --- a/src/locales/zh/statistics.ts +++ b/src/locales/zh/statistics.ts @@ -56,6 +56,7 @@ export const statistics = { playerSummaryUniqueTracks: '独立曲目', playerSummaryDays: 'Days', playerPartialIndexNotice: '部分服务器未纳入本地库索引,在这些服务器上的收听不会计入播放器统计。', + playerIndexRequired: '启用本地库索引后才会显示播放器统计。', playerPartialIndexSettings: '库设置', playerSummaryCompletion: '完整 / 部分', playerYearPrev: '上一年',