mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
refactor(stats): co-locate statistics feature into features/stats
This commit is contained in:
@@ -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
-7
@@ -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();
|
||||
|
||||
+2
-2
@@ -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;
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+4
-4
@@ -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. */
|
||||
+2
-2
@@ -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();
|
||||
+4
-4
@@ -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;
|
||||
+2
-2
@@ -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
-1
@@ -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) {
|
||||
+2
-2
@@ -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 {
|
||||
@@ -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
-1
@@ -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
-1
@@ -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,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 {
|
||||
|
||||
Reference in New Issue
Block a user