refactor(lib): consolidate generic infra into src/lib

Move domain-agnostic, feature-free helpers out of the flat utils/ and store/
roots into src/lib/ (plan M4, §3 lib/ layer):
- lib/format/: formatBytes, formatClockTime, formatDuration, formatHumanDuration,
  relativeTime (pure format/date/byte/clock helpers)
- lib/i18n.ts: i18next bootstrap (global app infra)
- lib/util/: sanitizeHtml, platform, dedupeById, safeStorage (pure helpers +
  the zustand storage adapter)

playbackScheduleFormat stays in utils/format (runtime usePlayerStore dep =
audio-core coupling, not generic). formatClockTime keeps a type-only
@/store/authStoreTypes import (erased, no runtime inversion — accepted per the
deviceSync precedent).

Pure move via deep @/lib/* specifiers (no barrel → no mock-collapse surface).
tsc 0, lint 0/0, full suite 319 files / 2353 tests green.

Tooling note: lib_move.py regex extended to also rewrite side-effect imports
(import './i18n') and vi.importActual paths — both were silent gaps.
This commit is contained in:
Psychotoxical
2026-06-30 08:07:04 +02:00
parent 8cc022581f
commit 209dd61442
103 changed files with 119 additions and 119 deletions
@@ -14,9 +14,9 @@ import StarRating from '@/components/StarRating';
import { copyEntityShareLink } from '@/utils/share/copyEntityShareLink';
import { showToast } from '@/utils/ui/toast';
import { isAlbumRecentlyAdded } from '@/features/album/utils/albumRecency';
import { formatLongDuration } from '@/utils/format/formatDuration';
import { formatMb } from '@/utils/format/formatBytes';
import { sanitizeHtml } from '@/utils/sanitizeHtml';
import { formatLongDuration } from '@/lib/format/formatDuration';
import { formatMb } from '@/lib/format/formatBytes';
import { sanitizeHtml } from '@/lib/util/sanitizeHtml';
import { OpenArtistRefInline } from '@/features/artist';
import { tooltipAttrs } from '@/ui/tooltipAttrs';
import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline';
+1 -1
View File
@@ -5,7 +5,7 @@ import { ChevronLeft, ChevronRight, ArrowRight } from 'lucide-react';
import { NavLink, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
import { dedupeById } from '@/utils/dedupeById';
import { dedupeById } from '@/lib/util/dedupeById';
interface Props {
title: string;
@@ -3,7 +3,7 @@ import { AudioLines } from 'lucide-react';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import type { Track } from '@/store/playerStoreTypes';
import { songToTrack } from '@/utils/playback/songToTrack';
import { formatLongDuration } from '@/utils/format/formatDuration';
import { formatLongDuration } from '@/lib/format/formatDuration';
interface Props {
discNums: number[];
+2 -2
View File
@@ -11,9 +11,9 @@ import { useThemeStore } from '@/store/themeStore';
import { previewInputFromSong, usePreviewStore } from '@/store/previewStore';
import StarRating from '@/components/StarRating';
import { codecLabel, type ColKey } from '@/features/album/utils/albumTrackListHelpers';
import { formatLongDuration } from '@/utils/format/formatDuration';
import { formatLongDuration } from '@/lib/format/formatDuration';
import { formatLastSeen } from '@/utils/componentHelpers/userMgmtHelpers';
import i18n from '@/i18n';
import i18n from '@/lib/i18n';
import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline';
import { resolveTrackArtistRefs } from '@/utils/playback/trackArtistRefs';
@@ -12,7 +12,7 @@ import {
coverTrafficGridPaginationDepth,
} from '@/cover/coverTraffic';
import { coverEnsureQueueBacklog, coverEnsureResumePump, coverEnsureSubscribeBacklogDrain } from '@/cover/ensureQueue';
import { dedupeById } from '@/utils/dedupeById';
import { dedupeById } from '@/lib/util/dedupeById';
import { albumBrowseCompScanComplete, albumBrowseCompFilterClientOnly } from '@/utils/library/albumCompilation';
import type { AlbumCompFilter } from '@/utils/library/albumCompilation';
import {
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { SubsonicAlbum } from '@/lib/api/subsonicTypes';
import { dedupeById } from '@/utils/dedupeById';
import { dedupeById } from '@/lib/util/dedupeById';
import type { AlbumBrowseSort } from '@/utils/library/albumBrowseSort';
import {
fetchGenreAlbumPage,
+1 -1
View File
@@ -3,7 +3,7 @@ import { getAlbumsByGenre } from '@/lib/api/subsonicGenres';
import { getAlbumList } from '@/lib/api/subsonicLibrary';
import { resolveAlbum } from '@/features/offline';
import type { SubsonicAlbum } from '@/lib/api/subsonicTypes';
import { dedupeById } from '@/utils/dedupeById';
import { dedupeById } from '@/lib/util/dedupeById';
import { shuffleArray } from '@/utils/playback/shuffleArray';
import React, { useEffect, useLayoutEffect, useState, useCallback, useRef } from 'react';
import { RefreshCw, Download, HardDriveDownload } from 'lucide-react';
@@ -6,7 +6,7 @@ import { usePlayerStore } from '@/store/playerStore';
import { previewInputFromSong, usePreviewStore } from '@/store/previewStore';
import { useOrbitSongRowBehavior } from '@/features/orbit';
import { songToTrack } from '@/utils/playback/songToTrack';
import { formatTrackTime } from '@/utils/format/formatDuration';
import { formatTrackTime } from '@/lib/format/formatDuration';
import ArtistTopTrackCover from '@/features/artist/components/ArtistTopTrackCover';
import { topSongAlbumForCover } from '@/features/artist/components/topSongAlbumForCover';
@@ -1,7 +1,7 @@
import { getArtists } from '@/lib/api/subsonicArtists';
import type { SubsonicArtist } from '@/lib/api/subsonicTypes';
import { useCallback, useEffect, useRef, useState } from 'react';
import { dedupeById } from '@/utils/dedupeById';
import { dedupeById } from '@/lib/util/dedupeById';
import {
fetchLocalArtistCatalogChunk,
fetchNetworkStarredArtists,
@@ -2,7 +2,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { AlertCircle, Loader2 } from 'lucide-react';
import type { SyncDelta } from '@/features/deviceSync/utils/runDeviceSyncExecution';
import { formatMb } from '@/utils/format/formatBytes';
import { formatMb } from '@/lib/format/formatBytes';
interface Props {
preSyncOpen: boolean;
@@ -1,4 +1,4 @@
import { IS_WINDOWS } from '@/utils/platform';
import { IS_WINDOWS } from '@/lib/util/platform';
// Same sanitize rules the Rust side uses (`sanitize_path_component`): strip
// Windows-illegal chars and control chars, trim leading/trailing dots + spaces.
@@ -5,7 +5,7 @@ import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { applyLegacyTemplate } from '@/features/deviceSync/utils/deviceSyncLegacyTemplate';
import { trackToSyncInfo } from '@/features/deviceSync/utils/deviceSyncHelpers';
import { fetchTracksForSource } from '@/utils/playback/fetchTracksForSource';
import { IS_WINDOWS } from '@/utils/platform';
import { IS_WINDOWS } from '@/lib/util/platform';
export type MigrationPhase = 'closed' | 'loading' | 'preview' | 'executing' | 'done' | 'nothing';
@@ -5,8 +5,8 @@ import type { ColDef } from '@/utils/useTracklistColumns';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { codecLabel } from '@/utils/componentHelpers/playlistDetailHelpers';
import { formatLastSeen } from '@/utils/componentHelpers/userMgmtHelpers';
import i18n from '@/i18n';
import { formatTrackTime } from '@/utils/format/formatDuration';
import i18n from '@/lib/i18n';
import { formatTrackTime } from '@/lib/format/formatDuration';
import StarRating from '@/components/StarRating';
import { OpenArtistRefInline } from '@/features/artist';
import { resolveTrackArtistRefs } from '@/utils/playback/trackArtistRefs';
@@ -1,7 +1,7 @@
import { memo, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '@/store/authStore';
import { formatClockTime } from '@/utils/format/formatClockTime';
import { formatClockTime } from '@/lib/format/formatClockTime';
/**
* Standalone wall-clock for the fullscreen player. Owns its own state so the
@@ -8,7 +8,7 @@ import {
getQueueResolverVersion,
subscribeQueueResolver,
} from '@/utils/library/queueTrackResolver';
import { formatTrackTime } from '@/utils/format/formatDuration';
import { formatTrackTime } from '@/lib/format/formatDuration';
interface Props {
onClose: () => void;
@@ -1,6 +1,6 @@
import { memo, useEffect, useRef } from 'react';
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '@/store/playbackProgress';
import { formatTrackTime } from '@/utils/format/formatDuration';
import { formatTrackTime } from '@/lib/format/formatDuration';
/**
* Centered "current / total" readout for the control bar. Updates the current
@@ -1,5 +1,5 @@
import { Pause, Play, SkipBack, SkipForward } from 'lucide-react';
import { formatTrackTime } from '@/utils/format/formatDuration';
import { formatTrackTime } from '@/lib/format/formatDuration';
import type { MiniControlAction } from '@/features/miniPlayer/utils/miniPlayerBridge';
interface Props {
@@ -1,6 +1,6 @@
import { Maximize2, Pin, PinOff, X } from 'lucide-react';
import type { TFunction } from 'i18next';
import { IS_LINUX } from '@/utils/platform';
import { IS_LINUX } from '@/lib/util/platform';
interface Props {
trackTitle: string | undefined;
@@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { useAuthStore } from '@/store/authStore';
import { IS_LINUX } from '@/utils/platform';
import { IS_LINUX } from '@/lib/util/platform';
import {
EXPANDED_SIZE, EXPANDED_MIN, readStoredExpandedHeight,
} from '@/features/miniPlayer/utils/miniPlayerHelpers';
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Disc3, ExternalLink, Star } from 'lucide-react';
import type { SubsonicAlbum, SubsonicSong } from '@/lib/api/subsonicTypes';
import { formatTotalDuration } from '@/utils/componentHelpers/nowPlayingHelpers';
import { formatTrackTime } from '@/utils/format/formatDuration';
import { formatTrackTime } from '@/lib/format/formatDuration';
interface AlbumCardProps {
album: SubsonicAlbum | null;
+1 -1
View File
@@ -6,7 +6,7 @@ import type { CoverArtRef } from '@/cover/types';
import type { ArtistStats, TrackStats } from '@/music-network';
import type { SubsonicOpenArtistRef } from '@/lib/api/subsonicTypes';
import { OpenArtistRefInline } from '@/features/artist';
import { formatTrackTime } from '@/utils/format/formatDuration';
import { formatTrackTime } from '@/lib/format/formatDuration';
import { useEnrichmentPrimaryLabel } from '@/hooks/useEnrichmentPrimaryLabel';
import { useEnrichmentPrimaryIcon } from '@/hooks/useEnrichmentPrimaryIcon';
import { renderPresetIcon } from '@/components/settings/musicNetwork/presetIcon';
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Cast, Clock, Radio, SkipForward, Users } from 'lucide-react';
import type { useRadioMetadata } from '@/features/radio';
import { usePlayerStore } from '@/store/playerStore';
import { formatTrackTime } from '@/utils/format/formatDuration';
import { formatTrackTime } from '@/lib/format/formatDuration';
type NonNullStoreField<K extends keyof ReturnType<typeof usePlayerStore.getState>> =
NonNullable<ReturnType<typeof usePlayerStore.getState>[K]>;
@@ -2,7 +2,7 @@ import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { ExternalLink, Play, TrendingUp } from 'lucide-react';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { formatTrackTime } from '@/utils/format/formatDuration';
import { formatTrackTime } from '@/lib/format/formatDuration';
interface TopSongsCardProps {
artistName: string;
@@ -2,7 +2,7 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { HardDriveDownload, Heart } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { formatBytes } from '@/utils/format/formatBytes';
import { formatBytes } from '@/lib/format/formatBytes';
const OPEN_DELAY_MS = 450;
@@ -2,7 +2,7 @@ import { useEffect } from 'react';
import { createPortal } from 'react-dom';
import { HardDriveDownload, Check, X } from 'lucide-react';
import { useZipDownloadStore } from '@/features/offline/store/zipDownloadStore';
import { formatBytes } from '@/utils/format/formatBytes';
import { formatBytes } from '@/lib/format/formatBytes';
function ZipDownloadItem({ id }: { id: string }) {
const dismiss = useZipDownloadStore(s => s.dismiss);
@@ -6,7 +6,7 @@ import { getStarredForServer } from '@/lib/api/subsonicStarRating';
import { buildStreamUrlForServer } from '@/lib/api/subsonicStreamUrl';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { invoke } from '@tauri-apps/api/core';
import i18n from '@/i18n';
import i18n from '@/lib/i18n';
import { useAuthStore } from '@/store/authStore';
import { cancelledDownloads, useOfflineJobStore } from '@/features/offline/store/offlineJobStore';
import { useFavoritesOfflineSyncStore } from '@/features/offline/store/favoritesOfflineSyncStore';
@@ -11,7 +11,7 @@ import {
albumToAlbum,
trackToSong,
} from '@/utils/library/advancedSearchLocal';
import { dedupeById } from '@/utils/dedupeById';
import { dedupeById } from '@/lib/util/dedupeById';
import { isOfflineBrowseActive } from '@/features/offline/utils/offlineBrowseMode';
import { favoritesServerIds } from '@/features/offline/utils/favoritesOfflineBrowse';
import {
@@ -19,7 +19,7 @@ import OrbitSettingsPopover from '@/features/orbit/components/OrbitSettingsPopov
import OrbitSharePopover from '@/features/orbit/components/OrbitSharePopover';
import OrbitDiagnosticsPopover from '@/features/orbit/components/OrbitDiagnosticsPopover';
import ConfirmModal from '@/components/ConfirmModal';
import { formatTrackTime } from '@/utils/format/formatDuration';
import { formatTrackTime } from '@/lib/format/formatDuration';
/**
* Orbit — top-strip session indicator.
+1 -1
View File
@@ -15,7 +15,7 @@ import {
resetPendingResendState,
} from '@/features/orbit/utils/orbit';
import { showToast } from '@/utils/ui/toast';
import i18n from '@/i18n';
import i18n from '@/lib/i18n';
import { estimateLivePosition, type OrbitState } from '@/features/orbit/api/orbit';
import { pushOrbitEvent } from '@/features/orbit/utils/orbitDiag';
import { useOrbitOutboxHeartbeat } from '@/features/orbit/hooks/useOrbitOutboxHeartbeat';
+1 -1
View File
@@ -20,7 +20,7 @@ import {
type OrbitQueueItem,
} from '@/features/orbit/api/orbit';
import { showToast } from '@/utils/ui/toast';
import i18n from '@/i18n';
import i18n from '@/lib/i18n';
import { pushOrbitEvent } from '@/features/orbit/utils/orbitDiag';
import { useOrbitOutboxHeartbeat } from '@/features/orbit/hooks/useOrbitOutboxHeartbeat';
+1 -1
View File
@@ -1,6 +1,6 @@
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { useConfirmModalStore } from '@/store/confirmModalStore';
import i18n from '@/i18n';
import i18n from '@/lib/i18n';
/**
* Ask the user before dropping many tracks into the shared Orbit queue.
@@ -7,7 +7,7 @@ import { usePlayerStore } from '@/store/playerStore';
import {
displayPlaylistName, isSmartPlaylistName, type PendingSmartPlaylist,
} from '@/features/playlist/utils/playlistsSmart';
import { formatHumanHoursMinutes } from '@/utils/format/formatHumanDuration';
import { formatHumanHoursMinutes } from '@/lib/format/formatHumanDuration';
import { useDragSource } from '@/contexts/DragDropContext';
import { PlaylistCardMainCover, PlaylistSmartCoverCell } from '@/features/playlist/components/PlaylistCoverImages';
@@ -5,8 +5,8 @@ import type { ColDef } from '@/utils/useTracklistColumns';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { codecLabel } from '@/utils/componentHelpers/playlistDetailHelpers';
import { formatLastSeen } from '@/utils/componentHelpers/userMgmtHelpers';
import i18n from '@/i18n';
import { formatTrackTime } from '@/utils/format/formatDuration';
import i18n from '@/lib/i18n';
import { formatTrackTime } from '@/lib/format/formatDuration';
import StarRating from '@/components/StarRating';
import { PlaylistArtistCell } from '@/features/playlist/components/PlaylistArtistCell';
@@ -4,7 +4,7 @@ import { Check, ListPlus, X } from 'lucide-react';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { usePlayerStore } from '@/store/playerStore';
import { songToTrack } from '@/utils/playback/songToTrack';
import { formatTrackTime } from '@/utils/format/formatDuration';
import { formatTrackTime } from '@/lib/format/formatDuration';
import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage';
import { COVER_DENSE_SEARCH_CSS_PX } from '@/cover/layoutSizes';
import { AddToPlaylistSubmenu } from '@/components/ContextMenu';
@@ -14,8 +14,8 @@ import { songToTrack } from '@/utils/playback/songToTrack';
import { getQueueTracksView } from '@/utils/library/queueTrackView';
import { codecLabel } from '@/utils/componentHelpers/playlistDetailHelpers';
import { formatLastSeen } from '@/utils/componentHelpers/userMgmtHelpers';
import { formatTrackTime } from '@/utils/format/formatDuration';
import i18n from '@/i18n';
import { formatTrackTime } from '@/lib/format/formatDuration';
import i18n from '@/lib/i18n';
const PL_CENTERED = new Set(['favorite', 'rating', 'duration', 'playCount', 'bpm']);
@@ -4,7 +4,7 @@ import { Music, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import type { ServerProfile } from '@/store/authStoreTypes';
import { formatTrackTime } from '@/utils/format/formatDuration';
import { formatTrackTime } from '@/lib/format/formatDuration';
import type { ShareQueuePreviewState } from '@/features/search/hooks/useShareQueuePreview';
import { sharePayloadTotal, type QueueableShareSearchPayload } from '@/utils/share/shareSearch';
import OverlayScrollArea from '@/ui/OverlayScrollArea';
@@ -10,7 +10,7 @@ import {
import type { SeekbarStyle, WindowButtonStyle } from '@/store/authStoreTypes';
import { useFontStore, FontId } from '@/store/fontStore';
import { useThemeStore } from '@/store/themeStore';
import { IS_LINUX, IS_WINDOWS } from '@/utils/platform';
import { IS_LINUX, IS_WINDOWS } from '@/lib/util/platform';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
@@ -8,7 +8,7 @@ import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
import { effectiveLoudnessPreAnalysisAttenuationDb } from '@/utils/audio/loudnessPreAnalysisSlider';
import { useAudioDevicesProbe } from '@/hooks/useAudioDevicesProbe';
import { IS_MACOS } from '@/utils/platform';
import { IS_MACOS } from '@/lib/util/platform';
import { AudioOutputDeviceSection } from '@/features/settings/components/audio/AudioOutputDeviceSection';
import { NormalizationBlock } from '@/features/settings/components/audio/NormalizationBlock';
import { PlaybackRateBlock } from '@/features/settings/components/audio/PlaybackRateBlock';
@@ -17,7 +17,7 @@ import { clearDiskSrcCacheForServer } from '@/cover/diskSrcCache';
import { serverListDisplayLabel } from '@/utils/server/serverDisplayName';
import { serverIndexKeyForProfile } from '@/utils/server/serverIndexKey';
import { showToast } from '@/utils/ui/toast';
import { formatBytes } from '@/utils/format/formatBytes';
import { formatBytes } from '@/lib/format/formatBytes';
import { clearImageCache, getImageCacheSize } from '@/utils/imageCache';
import { wakeLibraryCoverBackfill } from '@/utils/library/coverBackfillWake';
import {
@@ -6,7 +6,7 @@ import { Download, FolderOpen, Trash2, X } from 'lucide-react';
import { useAuthStore } from '@/store/authStore';
import { countHotCacheTracks } from '@/store/hotCacheStore';
import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { formatBytes, snapHotCacheMb } from '@/utils/format/formatBytes';
import { formatBytes, snapHotCacheMb } from '@/lib/format/formatBytes';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
@@ -6,10 +6,10 @@ import { save as saveDialog } from '@tauri-apps/plugin-dialog';
import { open as openUrl } from '@tauri-apps/plugin-shell';
import { AppWindow, ChevronDown, Download, ExternalLink, Globe, HardDrive, Info, Scale, Sliders, Users } from 'lucide-react';
import { version as appVersion } from '@/../package.json';
import i18n from '@/i18n';
import i18n from '@/lib/i18n';
import { useAuthStore } from '@/store/authStore';
import type { ClockFormat, LinuxWaylandTextRenderProfile, LoggingMode } from '@/store/authStoreTypes';
import { IS_LINUX } from '@/utils/platform';
import { IS_LINUX } from '@/lib/util/platform';
import { showToast } from '@/utils/ui/toast';
import { AboutPsysonicBrandHeader } from '@/components/AboutPsysonicLol';
import CustomSelect from '@/ui/CustomSelect';
@@ -6,7 +6,7 @@ import CoverLightbox from '@/components/CoverLightbox';
import { useThemeAnimationRisk } from '@/hooks/useThemeAnimationRisk';
import { AnimatedThemeBadge } from '@/features/settings/components/AnimatedThemeBadge';
import CustomSelect from '@/ui/CustomSelect';
import { formatRelativeTime } from '@/utils/format/relativeTime';
import { formatRelativeTime } from '@/lib/format/relativeTime';
import { useThemeStore } from '@/store/themeStore';
import { useInstalledThemesStore, type InstalledTheme } from '@/store/installedThemesStore';
import {
+1 -1
View File
@@ -5,7 +5,7 @@ import {
AudioLines, HardDrive, Info, Keyboard, LayoutGrid, Music2, Paintbrush, Palette, Search, Server, Sparkles, Users, X,
} from 'lucide-react';
import { useAuthStore } from '@/store/authStore';
import { IS_MACOS } from '@/utils/platform';
import { IS_MACOS } from '@/lib/util/platform';
import { AppearanceTab } from '@/features/settings/components/AppearanceTab';
import { ThemesTab } from '@/features/settings/components/ThemesTab';
import { AudioTab } from '@/features/settings/components/AudioTab';
@@ -13,7 +13,7 @@ import { usePlayerStatsRecordingEnabled } from '@/features/stats/hooks/usePlayer
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';
import { formatPlayerStatsListeningTotal } from '@/lib/format/formatHumanDuration';
const currentCalendarYear = () => new Date().getFullYear();
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import type { PlaySessionDayDetail } from '@/lib/api/library';
import { formatPlayerStatsListenedSec } from '@/utils/format/formatHumanDuration';
import { formatPlayerStatsListenedSec } from '@/lib/format/formatHumanDuration';
type Props = {
detail: PlaySessionDayDetail;
@@ -7,7 +7,7 @@ import {
type PlaySessionDayDetail,
type PlaySessionRecentDay,
} from '@/lib/api/library';
import { formatPlayerStatsListeningTotal } from '@/utils/format/formatHumanDuration';
import { formatPlayerStatsListeningTotal } from '@/lib/format/formatHumanDuration';
import {
formatPlayerStatsDayLabel,
PLAYER_STATS_RECENT_DAYS_LIMIT,
+1 -1
View File
@@ -4,7 +4,7 @@ import type { SubsonicAlbum, SubsonicGenre } from '@/lib/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 { formatHumanHoursMinutes } from '@/lib/format/formatHumanDuration';
import { AlbumRow } from '@/features/album';
import StatsExportModal from '@/features/stats/components/StatsExportModal';
import PlayerStatisticsPanel from '@/features/stats/components/PlayerStatisticsPanel';