mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
refactor(lib,playback): fold utils/cache+themes into lib; playbackScheduleFormat→playback (drains utils/format)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { getArtistInfoForServer } from '@/lib/api/subsonicArtists';
|
||||
import type { SubsonicArtistInfo, SubsonicOpenArtistRef } from '@/lib/api/subsonicTypes';
|
||||
import { makeCache } from '@/utils/cache/nowPlayingCache';
|
||||
import { makeCache } from '@/lib/cache/nowPlayingCache';
|
||||
|
||||
const artistInfoCache = makeCache<SubsonicArtistInfo | null>();
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Moon, Pause, Play, Sunrise } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import { usePlaybackDelayPress } from '@/features/playback/hooks/usePlaybackDelayPress';
|
||||
import { usePlaybackScheduleRemaining } from '@/utils/format/playbackScheduleFormat';
|
||||
import { usePlaybackScheduleRemaining } from '@/features/playback/utils/playbackScheduleFormat';
|
||||
import PlaybackDelayModal from '@/features/playback/components/PlaybackDelayModal';
|
||||
import PlaybackScheduleBadge from '@/features/playback/components/PlaybackScheduleBadge';
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import { LyricsPane } from '@/features/lyrics';
|
||||
import { usePlaybackDelayPress } from '@/features/playback/hooks/usePlaybackDelayPress';
|
||||
import PlaybackDelayModal from '@/features/playback/components/PlaybackDelayModal';
|
||||
import PlaybackScheduleBadge from '@/features/playback/components/PlaybackScheduleBadge';
|
||||
import { usePlaybackScheduleRemaining } from '@/utils/format/playbackScheduleFormat';
|
||||
import { usePlaybackScheduleRemaining } from '@/features/playback/utils/playbackScheduleFormat';
|
||||
|
||||
// ── Color extraction ──────────────────────────────────────────────────────────
|
||||
// Samples a 16×16 canvas to find the most vibrant (highest-saturation,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { resolveNpAlbum, resolveNpDiscography, resolveNpSongMeta, resolveNpTopSo
|
||||
import { fetchBandsintownEvents, type BandsintownEvent } from '@/api/bandsintown';
|
||||
import type { ArtistStats, TrackStats } from '@/music-network';
|
||||
import { getMusicNetworkRuntimeOrNull } from '@/music-network';
|
||||
import { makeCache } from '@/utils/cache/nowPlayingCache';
|
||||
import { makeCache } from '@/lib/cache/nowPlayingCache';
|
||||
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
|
||||
import { useConnectionStatus } from '@/hooks/useConnectionStatus';
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useAuthStore } from '@/store/authStore';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import type { TFunction } from 'i18next';
|
||||
import { formatPlaybackScheduleRemaining } from '@/utils/format/playbackScheduleFormat';
|
||||
import { formatPlaybackScheduleRemaining } from '@/features/playback/utils/playbackScheduleFormat';
|
||||
import { formatClockTime } from '@/lib/format/formatClockTime';
|
||||
import {
|
||||
isValidPlaybackSchedulePreviewTimestamp,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { formatPlaybackScheduleRemaining } from '@/utils/format/playbackScheduleFormat';
|
||||
import { formatPlaybackScheduleRemaining } from '@/features/playback/utils/playbackScheduleFormat';
|
||||
import { useWindowVisibility } from '@/lib/hooks/useWindowVisibility';
|
||||
|
||||
export interface PlaybackScheduleBadgeProps {
|
||||
|
||||
@@ -19,7 +19,7 @@ import { useRadioMetadata } from '@/features/radio';
|
||||
import { useRadioMprisSync } from '@/features/radio';
|
||||
import { usePlaybackDelayPress } from '@/features/playback/hooks/usePlaybackDelayPress';
|
||||
import PlaybackDelayModal from '@/features/playback/components/PlaybackDelayModal';
|
||||
import { usePlaybackScheduleRemaining } from '@/utils/format/playbackScheduleFormat';
|
||||
import { usePlaybackScheduleRemaining } from '@/features/playback/utils/playbackScheduleFormat';
|
||||
import { usePreviewStore } from '@/features/playback/store/previewStore';
|
||||
import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
|
||||
import { coerceOpenArtistRefs } from '@/lib/api/openArtistRefs';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useAutodjTransitionUi } from '@/features/playback/store/autodjTransitio
|
||||
import { usePreviewStore } from '@/features/playback/store/previewStore';
|
||||
import PlaybackScheduleBadge from '@/features/playback/components/PlaybackScheduleBadge';
|
||||
import { usePlaybackDelayPress } from '@/features/playback/hooks/usePlaybackDelayPress';
|
||||
import { usePlaybackScheduleRemaining } from '@/utils/format/playbackScheduleFormat';
|
||||
import { usePlaybackScheduleRemaining } from '@/features/playback/utils/playbackScheduleFormat';
|
||||
|
||||
type RepeatMode = PlayerState['repeatMode'];
|
||||
type PlayPauseBind = ReturnType<typeof usePlaybackDelayPress>['playPauseBind'];
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { resolveQueueTrack } from '@/features/playback/store/queueTrackView';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { getMusicNetworkRuntimeOrNull } from '@/music-network';
|
||||
import { setDeferHotCachePrefetch } from '@/utils/cache/hotCacheGate';
|
||||
import { setDeferHotCachePrefetch } from '@/lib/cache/hotCacheGate';
|
||||
import { notifyLibraryPlaybackHint } from '@/features/playback/store/libraryPlaybackHint';
|
||||
import {
|
||||
playListenSessionFinalize,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Track } from '@/lib/media/trackTypes';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { setDeferHotCachePrefetch } from '@/utils/cache/hotCacheGate';
|
||||
import { setDeferHotCachePrefetch } from '@/lib/cache/hotCacheGate';
|
||||
import {
|
||||
getPlaybackIndexKey,
|
||||
playbackCacheKeyForTrack,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { playbackReportStart, playbackReportStopped } from '@/features/playback/store/playbackReportSession';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { getMusicNetworkRuntimeOrNull } from '@/music-network';
|
||||
import { setDeferHotCachePrefetch } from '@/utils/cache/hotCacheGate';
|
||||
import { setDeferHotCachePrefetch } from '@/lib/cache/hotCacheGate';
|
||||
import { orbitBulkGuard, orbitSnapshot } from '@/store/orbitRuntime';
|
||||
import { sameQueueTrackId } from '@/features/playback/utils/playback/queueIdentity';
|
||||
import {
|
||||
|
||||
@@ -38,7 +38,7 @@ const hoisted = vi.hoisted(() => {
|
||||
});
|
||||
|
||||
vi.mock('@tauri-apps/api/core', () => ({ invoke: hoisted.invokeMock }));
|
||||
vi.mock('@/utils/cache/hotCacheGate', () => ({ setDeferHotCachePrefetch: hoisted.setDeferHotCachePrefetchMock }));
|
||||
vi.mock('@/lib/cache/hotCacheGate', () => ({ setDeferHotCachePrefetch: hoisted.setDeferHotCachePrefetchMock }));
|
||||
vi.mock('@/features/playback/utils/playback/resolvePlaybackUrl', () => ({ resolvePlaybackUrl: hoisted.resolvePlaybackUrlMock }));
|
||||
vi.mock('@/features/playback/utils/audio/resolveReplayGainDb', () => ({ resolveReplayGainDb: hoisted.resolveReplayGainDbMock }));
|
||||
vi.mock('@/store/authStore', () => ({ useAuthStore: { getState: () => hoisted.auth } }));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getSong } from '@/lib/api/subsonicLibrary';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { estimateLivePosition, orbitSnapshot } from '@/store/orbitRuntime';
|
||||
import { setDeferHotCachePrefetch } from '@/utils/cache/hotCacheGate';
|
||||
import { setDeferHotCachePrefetch } from '@/lib/cache/hotCacheGate';
|
||||
import {
|
||||
getPlaybackCacheServerKey,
|
||||
getPlaybackIndexKey,
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import { useWindowVisibility } from '@/lib/hooks/useWindowVisibility';
|
||||
|
||||
/** Remaining time until wall-clock `deadlineMs` (m:ss or h:mm:ss). */
|
||||
export function formatPlaybackScheduleRemaining(deadlineMs: number | null, nowMs: number): string {
|
||||
if (deadlineMs == null) return '';
|
||||
const sec = Math.max(0, Math.ceil((deadlineMs - nowMs) / 1000));
|
||||
const m = Math.floor(sec / 60);
|
||||
const s = sec % 60;
|
||||
if (m >= 60) {
|
||||
const h = Math.floor(m / 60);
|
||||
const rm = m % 60;
|
||||
return `${h}:${rm.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
||||
}
|
||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export interface PlaybackScheduleInfo {
|
||||
/** Formatted countdown, e.g. "4:32". */
|
||||
remaining: string;
|
||||
/** Which timer is running: `pause` = sleep-timer, `start` = delayed-start. */
|
||||
mode: 'pause' | 'start';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook: returns the active playback-schedule countdown + its mode, or null
|
||||
* when no timer is armed. Ticks every 500 ms so the caller never needs to
|
||||
* set up its own interval. Used by the play/pause button to swap the
|
||||
* Play/Pause icon for a mode-icon + countdown text while a timer runs.
|
||||
*/
|
||||
export function usePlaybackScheduleRemaining(): PlaybackScheduleInfo | null {
|
||||
const { isPlaying, scheduledPauseAtMs, scheduledResumeAtMs } = usePlayerStore(
|
||||
useShallow(s => ({
|
||||
isPlaying: s.isPlaying,
|
||||
scheduledPauseAtMs: s.scheduledPauseAtMs,
|
||||
scheduledResumeAtMs: s.scheduledResumeAtMs,
|
||||
})),
|
||||
);
|
||||
const mode: 'pause' | 'start' | null =
|
||||
isPlaying && scheduledPauseAtMs != null ? 'pause'
|
||||
: !isPlaying && scheduledResumeAtMs != null ? 'start'
|
||||
: null;
|
||||
const deadlineMs = mode === 'pause' ? scheduledPauseAtMs : mode === 'start' ? scheduledResumeAtMs : null;
|
||||
const [nowMs, setNowMs] = useState(() => Date.now());
|
||||
const windowHidden = useWindowVisibility();
|
||||
useEffect(() => {
|
||||
if (deadlineMs == null) return;
|
||||
// On a deadline change, snap the wall-clock to now so the countdown is fresh
|
||||
// before the 500 ms interval first fires; nowMs is real time, not derivable.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setNowMs(Date.now());
|
||||
}, [deadlineMs]);
|
||||
useEffect(() => {
|
||||
if (deadlineMs == null || windowHidden) return;
|
||||
const id = window.setInterval(() => {
|
||||
if (document.hidden || window.__psyHidden) return;
|
||||
setNowMs(Date.now());
|
||||
}, 500);
|
||||
return () => window.clearInterval(id);
|
||||
}, [deadlineMs, windowHidden]);
|
||||
if (mode == null || deadlineMs == null) return null;
|
||||
return { remaining: formatPlaybackScheduleRemaining(deadlineMs, nowMs), mode };
|
||||
}
|
||||
@@ -3,13 +3,13 @@ import { Check, RefreshCw, X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useInstalledThemesStore } from '@/store/installedThemesStore';
|
||||
import { uninstallTheme } from '@/utils/themes/uninstallTheme';
|
||||
import { installThemeFromRegistry } from '@/utils/themes/installThemeFromRegistry';
|
||||
import { uninstallTheme } from '@/lib/themes/uninstallTheme';
|
||||
import { installThemeFromRegistry } from '@/lib/themes/installThemeFromRegistry';
|
||||
import { useThemeUpdates } from '@/features/settings/hooks/useThemeUpdates';
|
||||
import { useThemeAnimationRisk } from '@/features/settings/hooks/useThemeAnimationRisk';
|
||||
import { showToast } from '@/utils/ui/toast';
|
||||
import { AnimatedThemeBadge } from '@/features/settings/components/AnimatedThemeBadge';
|
||||
import { FIXED_THEMES } from '@/utils/themes/fixedThemes';
|
||||
import { FIXED_THEMES } from '@/lib/themes/fixedThemes';
|
||||
|
||||
/** Pull a 3-band swatch (bg / card / accent) out of an installed theme's CSS. */
|
||||
function swatch(css: string): { bg: string; card: string; accent: string } {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { useInstalledThemesStore } from '@/store/installedThemesStore';
|
||||
import { validateThemePackage, type ValidatedTheme } from '@/utils/themes/validateThemePackage';
|
||||
import { validateThemePackage, type ValidatedTheme } from '@/lib/themes/validateThemePackage';
|
||||
import { showToast } from '@/utils/ui/toast';
|
||||
import ConfirmModal from '@/components/ConfirmModal';
|
||||
|
||||
|
||||
@@ -3,16 +3,16 @@ import { screen, waitFor, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { renderWithProviders } from '@/test/helpers/renderWithProviders';
|
||||
import { ThemeStoreSection } from '@/features/settings/components/ThemeStoreSection';
|
||||
import type { FetchRegistryResult, Registry, RegistryTheme } from '@/utils/themes/themeRegistry';
|
||||
import type { FetchRegistryResult, Registry, RegistryTheme } from '@/lib/themes/themeRegistry';
|
||||
|
||||
// Control the registry the store browses so pagination/refresh are deterministic.
|
||||
vi.mock('@/utils/themes/themeRegistry', () => ({
|
||||
vi.mock('@/lib/themes/themeRegistry', () => ({
|
||||
fetchRegistry: vi.fn(),
|
||||
fetchThemeCss: vi.fn(async () => 'css'),
|
||||
assetUrl: (p: string) => `https://raw.example/${p}`,
|
||||
}));
|
||||
|
||||
import { fetchRegistry } from '@/utils/themes/themeRegistry';
|
||||
import { fetchRegistry } from '@/lib/themes/themeRegistry';
|
||||
|
||||
const fetchRegistryMock = vi.mocked(fetchRegistry);
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
assetUrl,
|
||||
fetchRegistry,
|
||||
type RegistryTheme,
|
||||
} from '@/utils/themes/themeRegistry';
|
||||
import { installThemeFromRegistry } from '@/utils/themes/installThemeFromRegistry';
|
||||
import { uninstallTheme } from '@/utils/themes/uninstallTheme';
|
||||
} from '@/lib/themes/themeRegistry';
|
||||
import { installThemeFromRegistry } from '@/lib/themes/installThemeFromRegistry';
|
||||
import { uninstallTheme } from '@/lib/themes/uninstallTheme';
|
||||
import { isNewer } from '@/utils/componentHelpers/appUpdaterHelpers';
|
||||
|
||||
type ModeFilter = 'all' | 'dark' | 'light';
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useThemeStore } from '@/store/themeStore';
|
||||
import { useInstalledThemesStore } from '@/store/installedThemesStore';
|
||||
import CustomSelect from '@/ui/CustomSelect';
|
||||
import BackToTopButton from '@/ui/BackToTopButton';
|
||||
import { FIXED_THEMES } from '@/utils/themes/fixedThemes';
|
||||
import { FIXED_THEMES } from '@/lib/themes/fixedThemes';
|
||||
import { InstalledThemes } from '@/features/settings/components/InstalledThemes';
|
||||
import { ThemeImportSection } from '@/features/settings/components/ThemeImportSection';
|
||||
import { ThemeStoreSection } from '@/features/settings/components/ThemeStoreSection';
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
|
||||
vi.mock('@/utils/themes/themeRegistry', () => ({
|
||||
vi.mock('@/lib/themes/themeRegistry', () => ({
|
||||
fetchRegistry: vi.fn(),
|
||||
getCachedRegistry: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
import { fetchRegistry, type Registry } from '@/utils/themes/themeRegistry';
|
||||
import { fetchRegistry, type Registry } from '@/lib/themes/themeRegistry';
|
||||
import { useInstalledThemesStore, type InstalledTheme } from '@/store/installedThemesStore';
|
||||
import { useThemeUpdates, themeUpdateSignature } from '@/features/settings/hooks/useThemeUpdates';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { isNewer } from '@/utils/componentHelpers/appUpdaterHelpers';
|
||||
import { fetchRegistry, getCachedRegistry, type Registry, type RegistryTheme } from '@/utils/themes/themeRegistry';
|
||||
import { fetchRegistry, getCachedRegistry, type Registry, type RegistryTheme } from '@/lib/themes/themeRegistry';
|
||||
import { useInstalledThemesStore } from '@/store/installedThemesStore';
|
||||
|
||||
// Refresh the registry from source once per app launch (not just from the
|
||||
|
||||
Reference in New Issue
Block a user