refactor(stats): co-locate statistics feature into features/stats

This commit is contained in:
Psychotoxical
2026-06-29 22:51:54 +02:00
parent 431d65b587
commit 236acc33dd
19 changed files with 54 additions and 47 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ const Playlists = lazy(() => import('../pages/Playlists'));
const PlaylistDetail = lazy(() => import('../pages/PlaylistDetail'));
const NowPlayingPage = lazy(() => import('../pages/NowPlaying'));
const Settings = lazy(() => import('../pages/Settings'));
const Statistics = lazy(() => import('../pages/Statistics'));
const Statistics = lazy(() => import('@/features/stats/pages/Statistics'));
const Help = lazy(() => import('../pages/Help'));
const WhatsNew = lazy(() => import('../pages/WhatsNew'));
const DeviceSync = lazy(() => import('../pages/DeviceSync'));
@@ -1,7 +1,7 @@
import { useAuthStore } from '../store/authStore';
import { genreTagsFor } from '../utils/library/genreTags';
import { getArtists } from './subsonicArtists';
import { getAlbumList, getRandomSongs } from './subsonicLibrary';
import { useAuthStore } from '@/store/authStore';
import { genreTagsFor } from '@/utils/library/genreTags';
import { getArtists } from '@/api/subsonicArtists';
import { getAlbumList, getRandomSongs } from '@/api/subsonicLibrary';
import type {
StatisticsFormatSample,
StatisticsLibraryAggregates,
@@ -10,7 +10,7 @@ import type {
SubsonicArtist,
SubsonicGenre,
SubsonicSong,
} from './subsonicTypes';
} from '@/api/subsonicTypes';
/** Cache TTL for statistics page aggregates same 7-minute window as
* the rating prefetch cache in subsonicRatings.ts. */
@@ -7,13 +7,13 @@ import {
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 PlayerStatsRecentDays from './PlayerStatsRecentDays';
import { formatPlayerStatsListeningTotal } from '../../utils/format/formatHumanDuration';
} from '@/api/library';
import { usePlayerStatsLiveRefresh } from '@/features/stats/hooks/usePlayerStatsLiveRefresh';
import { usePlayerStatsRecordingEnabled } from '@/features/stats/hooks/usePlayerStatsRecordingEnabled';
import PlayerStatsHeatmap from '@/features/stats/components/PlayerStatsHeatmap';
import PlayerStatsIndexRequiredNotice from '@/features/stats/components/PlayerStatsIndexRequiredNotice';
import PlayerStatsRecentDays from '@/features/stats/components/PlayerStatsRecentDays';
import { formatPlayerStatsListeningTotal } from '@/utils/format/formatHumanDuration';
const currentCalendarYear = () => new Date().getFullYear();
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import type { PlaySessionDayDetail } from '../../api/library';
import { formatPlayerStatsListenedSec } from '../../utils/format/formatHumanDuration';
import type { PlaySessionDayDetail } from '@/api/library';
import { formatPlayerStatsListenedSec } from '@/utils/format/formatHumanDuration';
type Props = {
detail: PlaySessionDayDetail;
@@ -8,7 +8,7 @@ import {
heatmapWeekColumns,
heatmapWeekdayLabels,
type HeatmapCell,
} from '../../utils/playerStats/heatmapLevels';
} from '@/features/stats/utils/heatmapLevels';
const LEVEL_OPACITY = [0.14, 0.32, 0.52, 0.72, 1] as const;
@@ -6,13 +6,13 @@ import {
libraryGetPlayerStatsRecentDays,
type PlaySessionDayDetail,
type PlaySessionRecentDay,
} from '../../api/library';
import { formatPlayerStatsListeningTotal } from '../../utils/format/formatHumanDuration';
} from '@/api/library';
import { formatPlayerStatsListeningTotal } from '@/utils/format/formatHumanDuration';
import {
formatPlayerStatsDayLabel,
PLAYER_STATS_RECENT_DAYS_LIMIT,
} from '../../utils/playerStats/formatPlayerStatsDay';
import PlayerStatsDayTracks from './PlayerStatsDayTracks';
} from '@/features/stats/utils/formatPlayerStatsDay';
import PlayerStatsDayTracks from '@/features/stats/components/PlayerStatsDayTracks';
type Props = {
/** Day selected on the heatmap — auto-expand when present. */
@@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom';
import { useOfflineBrowseContext } from '../../hooks/useOfflineBrowseContext';
import { usePlayerStatsRecordingEnabled } from '../../hooks/usePlayerStatsRecordingEnabled';
import { useOfflineBrowseContext } from '@/hooks/useOfflineBrowseContext';
import { usePlayerStatsRecordingEnabled } from '@/features/stats/hooks/usePlayerStatsRecordingEnabled';
export default function StatisticsTabBar() {
const { t } = useTranslation();
@@ -1,18 +1,18 @@
import { getAlbumList } from '../api/subsonicLibrary';
import type { SubsonicAlbum } from '../api/subsonicTypes';
import { getAlbumList } from '@/api/subsonicLibrary';
import type { SubsonicAlbum } from '@/api/subsonicTypes';
import { useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { X } from 'lucide-react';
import { save } from '@tauri-apps/plugin-dialog';
import { writeFile } from '@tauri-apps/plugin-fs';
import { showToast } from '../utils/ui/toast';
import { showToast } from '@/utils/ui/toast';
import {
exportAlbumCardBlob,
renderAlbumCardCanvas,
ExportFormat,
ExportGridSize,
} from '../utils/export/exportAlbumCard';
} from '@/utils/export/exportAlbumCard';
interface Props {
open: boolean;
@@ -1,7 +1,7 @@
import { renderHook } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { usePlayerStatsLiveRefresh } from './usePlayerStatsLiveRefresh';
import { emitPlaySessionRecorded } from '../store/playSessionRecorded';
import { usePlayerStatsLiveRefresh } from '@/features/stats/hooks/usePlayerStatsLiveRefresh';
import { emitPlaySessionRecorded } from '@/store/playSessionRecorded';
describe('usePlayerStatsLiveRefresh', () => {
it('refreshes when a play session is recorded and the tab is visible', () => {
@@ -1,5 +1,5 @@
import { useEffect, useRef } from 'react';
import { onPlaySessionRecorded } from '../store/playSessionRecorded';
import { onPlaySessionRecorded } from '@/store/playSessionRecorded';
/** Refresh player stats when a listen is persisted or the tab becomes visible again. */
export function usePlayerStatsLiveRefresh(onRefresh: () => void) {
@@ -1,5 +1,5 @@
import { useAuthStore } from '../store/authStore';
import { useLibraryIndexStore } from '../store/libraryIndexStore';
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 {
+7
View File
@@ -0,0 +1,7 @@
/**
* Statistics feature — listening stats dashboard (player-stats heatmap, recent
* days, library aggregates) and its recording-enabled gate. The `Statistics`
* page is loaded lazily by the router via its deep path, so it is intentionally
* not re-exported here.
*/
export { usePlayerStatsRecordingEnabled } from './hooks/usePlayerStatsRecordingEnabled';
@@ -1,21 +1,21 @@
import { fetchStatisticsFormatSample, fetchStatisticsLibraryAggregates, fetchStatisticsOverview } from '../api/subsonicStatistics';
import { getAlbumList } from '../api/subsonicLibrary';
import type { SubsonicAlbum, SubsonicGenre } from '../api/subsonicTypes';
import { fetchStatisticsFormatSample, fetchStatisticsLibraryAggregates, fetchStatisticsOverview } from '@/features/stats/api/subsonicStatistics';
import { getAlbumList } from '@/api/subsonicLibrary';
import type { SubsonicAlbum, SubsonicGenre } from '@/api/subsonicTypes';
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Share2 } from 'lucide-react';
import { formatHumanHoursMinutes } from '../utils/format/formatHumanDuration';
import AlbumRow from '../components/AlbumRow';
import StatsExportModal from '../components/StatsExportModal';
import PlayerStatisticsPanel from '../components/statistics/PlayerStatisticsPanel';
import StatisticsTabBar from '../components/statistics/StatisticsTabBar';
import { formatHumanHoursMinutes } from '@/utils/format/formatHumanDuration';
import AlbumRow from '@/components/AlbumRow';
import StatsExportModal from '@/features/stats/components/StatsExportModal';
import PlayerStatisticsPanel from '@/features/stats/components/PlayerStatisticsPanel';
import StatisticsTabBar from '@/features/stats/components/StatisticsTabBar';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/authStore';
import { useAuthStore } from '@/store/authStore';
import { useLocation } from 'react-router-dom';
import { getMusicNetworkRuntime, type RecentTrack, type StatsPeriod, type TopItem } from '../music-network';
import { useOfflineBrowseContext } from '../hooks/useOfflineBrowseContext';
import { usePlayerStatsRecordingEnabled } from '../hooks/usePlayerStatsRecordingEnabled';
import { useEnrichmentPrimaryLabel } from '../hooks/useEnrichmentPrimaryLabel';
import { getMusicNetworkRuntime, type RecentTrack, type StatsPeriod, type TopItem } from '@/music-network';
import { useOfflineBrowseContext } from '@/hooks/useOfflineBrowseContext';
import { usePlayerStatsRecordingEnabled } from '@/features/stats/hooks/usePlayerStatsRecordingEnabled';
import { useEnrichmentPrimaryLabel } from '@/hooks/useEnrichmentPrimaryLabel';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function relativeTime(timestamp: number, t: (key: string, opts?: any) => string): string {
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { formatPlayerStatsDayLabel } from './formatPlayerStatsDay';
import { formatPlayerStatsDayLabel } from '@/features/stats/utils/formatPlayerStatsDay';
const t = (key: string) => {
if (key === 'statistics.playerDayToday') return 'Today';
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { heatmapCellMetrics } from './heatmapLevels';
import { heatmapCellMetrics } from '@/features/stats/utils/heatmapLevels';
describe('heatmapCellMetrics', () => {
it('shrinks cells to fit narrow containers', () => {
+1 -1
View File
@@ -1,7 +1,7 @@
import { useAuthStore } from '../store/authStore';
import { useOfflineStore } from '../store/offlineStore';
import { useConnectionStatus } from './useConnectionStatus';
import { usePlayerStatsRecordingEnabled } from './usePlayerStatsRecordingEnabled';
import { usePlayerStatsRecordingEnabled } from '@/features/stats';
import { hasOfflineBrowsingContent } from '../utils/offline/favoritesOfflineBrowse';
import { useOfflineBrowseActive } from '../utils/offline/offlineBrowseMode';
import {