mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 14:05:41 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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)',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -67,6 +67,7 @@ export const statistics = {
|
||||
playerSummaryUniqueTracks: 'Уникальные треки',
|
||||
playerSummaryDays: 'Days',
|
||||
playerPartialIndexNotice: 'Не все серверы включены в локальный индекс библиотеки. Прослушивание с выключенных серверов не попадает в эту статистику.',
|
||||
playerIndexRequired: 'Статистика плеера недоступна, пока не включён локальный индекс библиотеки.',
|
||||
playerPartialIndexSettings: 'Настройки библиотеки',
|
||||
playerSummaryCompletion: 'Полные / частичные',
|
||||
playerYearPrev: 'Предыдущий год',
|
||||
|
||||
@@ -56,6 +56,7 @@ export const statistics = {
|
||||
playerSummaryUniqueTracks: '独立曲目',
|
||||
playerSummaryDays: 'Days',
|
||||
playerPartialIndexNotice: '部分服务器未纳入本地库索引,在这些服务器上的收听不会计入播放器统计。',
|
||||
playerIndexRequired: '启用本地库索引后才会显示播放器统计。',
|
||||
playerPartialIndexSettings: '库设置',
|
||||
playerSummaryCompletion: '完整 / 部分',
|
||||
playerYearPrev: '上一年',
|
||||
|
||||
Reference in New Issue
Block a user