refactor(orbit): co-locate orbit feature into features/orbit

This commit is contained in:
Psychotoxical
2026-06-30 01:02:58 +02:00
parent d6dbb615fd
commit 321d5ff6ab
93 changed files with 295 additions and 248 deletions
+6 -8
View File
@@ -20,8 +20,7 @@ import SongInfoModal from '../components/SongInfoModal';
import { DownloadFolderModal } from '@/features/offline';
import GlobalConfirmModal from '../components/GlobalConfirmModal';
import ThemeMigrationNotice from '../components/ThemeMigrationNotice';
import OrbitAccountPicker from '../components/OrbitAccountPicker';
import OrbitHelpModal from '../components/OrbitHelpModal';
import { OrbitAccountPicker, OrbitHelpModal } from '@/features/orbit';
import TooltipPortal from '@/ui/TooltipPortal';
import OverlayScrollArea from '@/ui/OverlayScrollArea';
import {
@@ -33,11 +32,10 @@ import MusicNetworkIndicator from '../components/MusicNetworkIndicator';
import { OfflineBanner } from '@/features/offline';
import AppUpdater from '../components/AppUpdater';
import TitleBar from '../components/TitleBar';
import OrbitSessionBar from '../components/OrbitSessionBar';
import OrbitStartTrigger from '../components/OrbitStartTrigger';
import { useOrbitHost } from '../hooks/useOrbitHost';
import { useOrbitGuest } from '../hooks/useOrbitGuest';
import { useOrbitBodyAttrs } from '../hooks/useOrbitBodyAttrs';
import { OrbitSessionBar, OrbitStartTrigger } from '@/features/orbit';
import { useOrbitHost } from '@/features/orbit';
import { useOrbitGuest } from '@/features/orbit';
import { useOrbitBodyAttrs } from '@/features/orbit';
import { usePlatformShellSetup } from '../hooks/usePlatformShellSetup';
import { useOfflineBrowseContext } from '@/features/offline';
import { offlineBrowseNavFlags } from '@/features/offline';
@@ -67,7 +65,7 @@ import { useThemeStore } from '../store/themeStore';
import { useFontStore } from '../store/fontStore';
import { useEqStore } from '../store/eqStore';
import { usePlaybackRateStore } from '../store/playbackRateStore';
import { usePlaybackRateOrbitSync } from '../hooks/usePlaybackRateOrbitSync';
import { usePlaybackRateOrbitSync } from '@/features/orbit';
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
import {
persistSidebarCollapsed,
+1 -1
View File
@@ -30,7 +30,7 @@ vi.mock('@/api/subsonic', () => ({
}));
vi.mock('@/utils/orbitBulkGuard', () => ({
vi.mock('@/features/orbit', () => ({
orbitBulkGuard: vi.fn(async () => true),
}));
+1 -1
View File
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { Track } from '../store/playerStoreTypes';
import { useOrbitStore } from '../store/orbitStore';
import { useOrbitStore } from '@/features/orbit';
import { usePlayerStore } from '../store/playerStore';
import { useShallow } from 'zustand/react/shallow';
import { useAuthStore } from '../store/authStore';
+3 -3
View File
@@ -3,14 +3,14 @@ import type { SubsonicSong } from '../api/subsonicTypes';
import { useEffect, useMemo, useState } from 'react';
import { Check, X, Inbox } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useOrbitStore } from '../store/orbitStore';
import { useOrbitStore } from '@/features/orbit';
import {
approveOrbitSuggestion,
declineOrbitSuggestion,
suggestionKey,
} from '../utils/orbit';
} from '@/features/orbit';
import { TrackCoverArtImage } from '../cover/TrackCoverArtImage';
import { ORBIT_DEFAULT_SETTINGS } from '../api/orbit';
import { ORBIT_DEFAULT_SETTINGS } from '@/features/orbit';
const HOST_APPROVAL_COVER_CSS_PX = 36;
+2 -2
View File
@@ -16,9 +16,9 @@ import {
findSessionPlaylistId,
readOrbitState,
OrbitJoinError,
} from '../utils/orbit';
} from '@/features/orbit';
import { switchActiveServer } from '../utils/server/switchActiveServer';
import { useOrbitAccountPickerStore } from '../store/orbitAccountPickerStore';
import { useOrbitAccountPickerStore } from '@/features/orbit';
import ConfirmModal from './ConfirmModal';
const ORBIT_JOIN_ERROR_KEYS: Record<string, string> = {
+1 -1
View File
@@ -26,7 +26,7 @@ vi.mock('@/api/subsonic', () => ({
}));
vi.mock('@/utils/orbitBulkGuard', () => ({
vi.mock('@/features/orbit', () => ({
orbitBulkGuard: vi.fn(async () => true),
}));
+2 -3
View File
@@ -5,9 +5,8 @@ import { songToTrack } from '../utils/playback/songToTrack';
import type { Track } from '../store/playerStoreTypes';
import { useState, useRef, useMemo } from 'react';
import { usePlayerStore } from '../store/playerStore';
import { useOrbitStore } from '../store/orbitStore';
import OrbitGuestQueue from './OrbitGuestQueue';
import OrbitQueueHead from './OrbitQueueHead';
import { useOrbitStore } from '@/features/orbit';
import { OrbitGuestQueue, OrbitQueueHead } from '@/features/orbit';
import HostApprovalQueue from './HostApprovalQueue';
import { usePlaylistStore } from '../store/playlistStore';
import { useTranslation } from 'react-i18next';
+1 -1
View File
@@ -10,7 +10,7 @@ import { useTrackCoverRef } from '../cover/useLibraryCoverRef';
import { COVER_DENSE_RAIL_CELL_CSS_PX } from '../cover/layoutSizes';
import { enqueueAndPlay } from '../utils/playback/playSong';
import { useDragDrop } from '../contexts/DragDropContext';
import { useOrbitSongRowBehavior } from '../hooks/useOrbitSongRowBehavior';
import { useOrbitSongRowBehavior } from '@/features/orbit';
import { useNavigateToAlbum } from '../hooks/useNavigateToAlbum';
import { useNavigateToArtist } from '../hooks/useNavigateToArtist';
import { OpenArtistRefInline } from './OpenArtistRefInline';
+1 -1
View File
@@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next';
import { usePlayerStore } from '../store/playerStore';
import { enqueueAndPlay } from '../utils/playback/playSong';
import { useDragDrop } from '../contexts/DragDropContext';
import { useOrbitSongRowBehavior } from '../hooks/useOrbitSongRowBehavior';
import { useOrbitSongRowBehavior } from '@/features/orbit';
import { formatTrackTime } from '../utils/format/formatDuration';
import { resolveTrackArtistRefs } from '../utils/playback/trackArtistRefs';
import { tooltipAttrs } from '@/ui/tooltipAttrs';
@@ -4,7 +4,7 @@ import { AudioLines, ChevronRight, Play, Square } from 'lucide-react';
import type { SubsonicAlbum, SubsonicSong } from '../../api/subsonicTypes';
import { usePlayerStore } from '../../store/playerStore';
import { previewInputFromSong, usePreviewStore } from '../../store/previewStore';
import { useOrbitSongRowBehavior } from '../../hooks/useOrbitSongRowBehavior';
import { useOrbitSongRowBehavior } from '@/features/orbit';
import { songToTrack } from '../../utils/playback/songToTrack';
import { formatTrackTime } from '../../utils/format/formatDuration';
import ArtistTopTrackCover from './ArtistTopTrackCover';
@@ -10,7 +10,7 @@ import { useAuthStore } from '../../store/authStore';
import { usePlaylistStore } from '../../store/playlistStore';
import { songToTrack } from '../../utils/playback/songToTrack';
import { showToast } from '../../utils/ui/toast';
import { suggestOrbitTrack, hostEnqueueToOrbit, evaluateOrbitSuggestGate, OrbitSuggestBlockedError } from '../../utils/orbit';
import { suggestOrbitTrack, hostEnqueueToOrbit, evaluateOrbitSuggestGate, OrbitSuggestBlockedError } from '@/features/orbit';
import { renderPresetIcon } from '../settings/musicNetwork/presetIcon';
import StarRating from '../StarRating';
import { AddToPlaylistSubmenu } from './AddToPlaylistSubmenu';
@@ -3,7 +3,7 @@ import { createPortal } from 'react-dom';
import type { TFunction } from 'i18next';
import { PlaybackRateControls } from '@/features/settings';
import { usePlaybackRateStore } from '../../store/playbackRateStore';
import { useOrbitStore } from '../../store/orbitStore';
import { useOrbitStore } from '@/features/orbit';
import {
clampPlaybackPitch,
clampPlaybackSpeed,
@@ -14,7 +14,7 @@ import {
playbackSpeedStep,
varispeedSpeedFromSemitones,
} from '../../utils/audio/playbackRateHelpers';
import { isOrbitPlaybackSyncActive } from '../../utils/orbit';
import { isOrbitPlaybackSyncActive } from '@/features/orbit';
import { usePlayerBarAnchoredPopover } from '../../hooks/usePlayerBarAnchoredPopover';
const POPOVER_WIDTH = 320;
@@ -16,7 +16,7 @@ import { usePlayerStore } from '../../store/playerStore';
import { previewInputFromSong, usePreviewStore } from '../../store/previewStore';
import { useThemeStore } from '../../store/themeStore';
import { useDragDrop } from '../../contexts/DragDropContext';
import { useOrbitSongRowBehavior } from '../../hooks/useOrbitSongRowBehavior';
import { useOrbitSongRowBehavior } from '@/features/orbit';
import { songToTrack } from '../../utils/playback/songToTrack';
import type { PlaylistSortKey, PlaylistSortDir } from '../../utils/playlist/playlistDisplayedSongs';
import { AddToPlaylistSubmenu } from '../ContextMenu';
+1 -1
View File
@@ -9,7 +9,7 @@ import type {
QueueToolbarButtonId,
} from '../../store/queueToolbarStore';
import { getTransitionMode, setTransitionMode } from '../../utils/playback/playbackTransition';
import { useOrbitStore } from '../../store/orbitStore';
import { useOrbitStore } from '@/features/orbit';
interface Props {
queue: QueueItemRef[];
@@ -11,7 +11,7 @@ import { previewInputFromSong, usePreviewStore } from '@/store/previewStore';
import { useSelectionStore } from '@/store/selectionStore';
import { useThemeStore } from '@/store/themeStore';
import { useDragDrop } from '@/contexts/DragDropContext';
import { useOrbitSongRowBehavior } from '@/hooks/useOrbitSongRowBehavior';
import { useOrbitSongRowBehavior } from '@/features/orbit';
import { songToTrack } from '@/utils/playback/songToTrack';
import { appendServerQuery } from '@/utils/navigation/detailServerScope';
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '@/constants/appScroll';
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { X, User } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useOrbitAccountPickerStore } from '../store/orbitAccountPickerStore';
import { useOrbitAccountPickerStore } from '@/features/orbit/store/orbitAccountPickerStore';
/**
* Modal shown when joining an Orbit session and the user has more than
@@ -2,16 +2,16 @@ import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
import { createPortal } from 'react-dom';
import { Copy, Trash2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useOrbitStore } from '../store/orbitStore';
import { usePlayerStore } from '../store/playerStore';
import { showToast } from '../utils/ui/toast';
import { computeOrbitDriftMs } from '../utils/orbit';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { usePlayerStore } from '@/store/playerStore';
import { showToast } from '@/utils/ui/toast';
import { computeOrbitDriftMs } from '@/features/orbit/utils/orbit';
import {
clearOrbitEvents,
formatOrbitEvents,
getOrbitEvents,
subscribeOrbitEvents,
} from '../utils/orbitDiag';
} from '@/features/orbit/utils/orbitDiag';
interface Props {
anchorRef: React.RefObject<HTMLElement | null>;
@@ -1,8 +1,8 @@
import { useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { useOrbitStore } from '../store/orbitStore';
import { leaveOrbitSession } from '../utils/orbit';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { leaveOrbitSession } from '@/features/orbit/utils/orbit';
/**
* Orbit exit notification modal.
@@ -1,11 +1,11 @@
import { getSong } from '../api/subsonicLibrary';
import type { SubsonicSong } from '../api/subsonicTypes';
import { getSong } from '@/api/subsonicLibrary';
import type { SubsonicSong } from '@/api/subsonicTypes';
import { useEffect, useMemo, useState } from 'react';
import { Radio, Clock } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useOrbitStore } from '../store/orbitStore';
import { TrackCoverArtImage } from '../cover/TrackCoverArtImage';
import OrbitQueueHead from './OrbitQueueHead';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { TrackCoverArtImage } from '@/cover/TrackCoverArtImage';
import OrbitQueueHead from '@/features/orbit/components/OrbitQueueHead';
const ORBIT_QUEUE_COVER_LG_CSS_PX = 54;
const ORBIT_QUEUE_COVER_SM_CSS_PX = 48;
@@ -5,7 +5,7 @@ import {
ListMusic, Inbox, Sliders, LogOut,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useHelpModalStore } from '../store/helpModalStore';
import { useHelpModalStore } from '@/store/helpModalStore';
import { SettingsSubSection } from '@/features/settings';
/**
@@ -2,16 +2,16 @@ import { useState } from 'react';
import { createPortal } from 'react-dom';
import { X, LogIn, ClipboardPaste } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/authStore';
import { useAuthStore } from '@/store/authStore';
import {
parseOrbitShareLink,
findSessionPlaylistId,
readOrbitState,
joinOrbitSession,
} from '../utils/orbit';
import { switchActiveServer } from '../utils/server/switchActiveServer';
import { useOrbitAccountPickerStore } from '../store/orbitAccountPickerStore';
import { showToast } from '../utils/ui/toast';
} from '@/features/orbit/utils/orbit';
import { switchActiveServer } from '@/utils/server/switchActiveServer';
import { useOrbitAccountPickerStore } from '@/features/orbit/store/orbitAccountPickerStore';
import { showToast } from '@/utils/ui/toast';
interface Props {
onClose: () => void;
@@ -2,9 +2,9 @@ import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Crown, User, UserMinus, ShieldOff, Mic, MicOff } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useOrbitStore } from '../store/orbitStore';
import { kickOrbitParticipant, removeOrbitParticipant, setOrbitSuggestionBlocked } from '../utils/orbit';
import ConfirmModal from './ConfirmModal';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { kickOrbitParticipant, removeOrbitParticipant, setOrbitSuggestionBlocked } from '@/features/orbit/utils/orbit';
import ConfirmModal from '@/components/ConfirmModal';
interface Props {
/** Anchor — we position the popover directly below its bottom-right. */
@@ -1,8 +1,8 @@
import { useEffect, useState } from 'react';
import { Users, Wifi, WifiOff } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useOrbitStore } from '../store/orbitStore';
import type { OrbitState } from '../api/orbit';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import type { OrbitState } from '@/features/orbit/api/orbit';
interface Props {
state: OrbitState;
@@ -1,25 +1,25 @@
import { getSong } from '../api/subsonicLibrary';
import { songToTrack } from '../utils/playback/songToTrack';
import { getSong } from '@/api/subsonicLibrary';
import { songToTrack } from '@/utils/playback/songToTrack';
import { useEffect, useRef, useState } from 'react';
import { X, RefreshCw, Shuffle, Settings2, Share2, HelpCircle, Activity } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useOrbitStore } from '../store/orbitStore';
import { useHelpModalStore } from '../store/helpModalStore';
import { usePlayerStore } from '../store/playerStore';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { useHelpModalStore } from '@/store/helpModalStore';
import { usePlayerStore } from '@/store/playerStore';
import {
endOrbitSession,
leaveOrbitSession,
computeOrbitDriftMs,
effectiveShuffleIntervalMs,
} from '../utils/orbit';
import { estimateLivePosition } from '../api/orbit';
import OrbitParticipantsPopover from './OrbitParticipantsPopover';
import OrbitExitModal from './OrbitExitModal';
import OrbitSettingsPopover from './OrbitSettingsPopover';
import OrbitSharePopover from './OrbitSharePopover';
import OrbitDiagnosticsPopover from './OrbitDiagnosticsPopover';
import ConfirmModal from './ConfirmModal';
import { formatTrackTime } from '../utils/format/formatDuration';
} from '@/features/orbit/utils/orbit';
import { estimateLivePosition } from '@/features/orbit/api/orbit';
import OrbitParticipantsPopover from '@/features/orbit/components/OrbitParticipantsPopover';
import OrbitExitModal from '@/features/orbit/components/OrbitExitModal';
import OrbitSettingsPopover from '@/features/orbit/components/OrbitSettingsPopover';
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';
/**
* Orbit top-strip session indicator.
@@ -2,10 +2,10 @@ import { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import { Shuffle } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useOrbitStore } from '../store/orbitStore';
import { updateOrbitSettings, triggerOrbitShuffleNow } from '../utils/orbit';
import { ORBIT_DEFAULT_SETTINGS, ORBIT_SHUFFLE_INTERVAL_PRESETS_MIN, type OrbitShuffleIntervalMin } from '../api/orbit';
import { showToast } from '../utils/ui/toast';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { updateOrbitSettings, triggerOrbitShuffleNow } from '@/features/orbit/utils/orbit';
import { ORBIT_DEFAULT_SETTINGS, ORBIT_SHUFFLE_INTERVAL_PRESETS_MIN, type OrbitShuffleIntervalMin } from '@/features/orbit/api/orbit';
import { showToast } from '@/utils/ui/toast';
interface Props {
anchorRef: React.RefObject<HTMLElement | null>;
@@ -2,10 +2,10 @@ import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Copy, Check } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useOrbitStore } from '../store/orbitStore';
import { useAuthStore } from '../store/authStore';
import { buildOrbitShareLink } from '../utils/orbit';
import { serverShareBaseUrl } from '../utils/server/serverEndpoint';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { useAuthStore } from '@/store/authStore';
import { buildOrbitShareLink } from '@/features/orbit/utils/orbit';
import { serverShareBaseUrl } from '@/utils/server/serverEndpoint';
interface Props {
anchorRef: React.RefObject<HTMLElement | null>;
@@ -9,12 +9,12 @@ import {
startOrbitSession,
buildOrbitShareLink,
generateSessionId,
} from '../utils/orbit';
import { randomOrbitSessionName } from '../utils/orbitNames';
import { useAuthStore } from '../store/authStore';
import { usePlayerStore } from '../store/playerStore';
import { isLanUrl, serverShareBaseUrl } from '../utils/server/serverEndpoint';
import { ORBIT_DEFAULT_MAX_USERS } from '../api/orbit';
} from '@/features/orbit/utils/orbit';
import { randomOrbitSessionName } from '@/features/orbit/utils/orbitNames';
import { useAuthStore } from '@/store/authStore';
import { usePlayerStore } from '@/store/playerStore';
import { isLanUrl, serverShareBaseUrl } from '@/utils/server/serverEndpoint';
import { ORBIT_DEFAULT_MAX_USERS } from '@/features/orbit/api/orbit';
interface Props { onClose: () => void; }
@@ -2,12 +2,12 @@ import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Orbit as OrbitIcon, Plus, LogIn, HelpCircle } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useOrbitStore } from '../store/orbitStore';
import { useAuthStore } from '../store/authStore';
import { useHelpModalStore } from '../store/helpModalStore';
import OrbitStartModal from './OrbitStartModal';
import OrbitJoinModal from './OrbitJoinModal';
import OrbitWordmark from './OrbitWordmark';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { useAuthStore } from '@/store/authStore';
import { useHelpModalStore } from '@/store/helpModalStore';
import OrbitStartModal from '@/features/orbit/components/OrbitStartModal';
import OrbitJoinModal from '@/features/orbit/components/OrbitJoinModal';
import OrbitWordmark from '@/features/orbit/components/OrbitWordmark';
/**
* Topbar trigger opens a small launch popover offering three choices:
@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { useOrbitStore } from '../store/orbitStore';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
/**
* Mirror the live Orbit role + phase onto `<html data-orbit-active>` and
@@ -1,9 +1,9 @@
import { getSong } from '../api/subsonicLibrary';
import { songToTrack } from '../utils/playback/songToTrack';
import { getSong } from '@/api/subsonicLibrary';
import { songToTrack } from '@/utils/playback/songToTrack';
import { useEffect, useRef } from 'react';
import { useOrbitStore } from '../store/orbitStore';
import { useAuthStore } from '../store/authStore';
import { usePlayerStore } from '../store/playerStore';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { useAuthStore } from '@/store/authStore';
import { usePlayerStore } from '@/store/playerStore';
import {
readOrbitState,
applyOrbitTransitionSettings,
@@ -13,12 +13,12 @@ import {
planPendingResends,
forgetPendingSuggestion,
resetPendingResendState,
} from '../utils/orbit';
import { showToast } from '../utils/ui/toast';
import i18n from '../i18n';
import { estimateLivePosition, type OrbitState } from '../api/orbit';
import { pushOrbitEvent } from '../utils/orbitDiag';
import { useOrbitOutboxHeartbeat } from './useOrbitOutboxHeartbeat';
} from '@/features/orbit/utils/orbit';
import { showToast } from '@/utils/ui/toast';
import i18n from '@/i18n';
import { estimateLivePosition, type OrbitState } from '@/features/orbit/api/orbit';
import { pushOrbitEvent } from '@/features/orbit/utils/orbitDiag';
import { useOrbitOutboxHeartbeat } from '@/features/orbit/hooks/useOrbitOutboxHeartbeat';
/**
* Orbit guest-side tick hook.
@@ -1,8 +1,8 @@
import { getSong } from '../api/subsonicLibrary';
import { songToTrack } from '../utils/playback/songToTrack';
import { getSong } from '@/api/subsonicLibrary';
import { songToTrack } from '@/utils/playback/songToTrack';
import { useEffect } from 'react';
import { useOrbitStore } from '../store/orbitStore';
import { usePlayerStore } from '../store/playerStore';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { usePlayerStore } from '@/store/playerStore';
import {
writeOrbitState,
sweepGuestOutboxes,
@@ -12,17 +12,17 @@ import {
makeCoalescedRunner,
readOrbitTransitionSettings,
suggestionKey,
} from '../utils/orbit';
} from '@/features/orbit/utils/orbit';
import {
ORBIT_DEFAULT_SETTINGS,
ORBIT_PLAY_QUEUE_LIMIT,
type OrbitState,
type OrbitQueueItem,
} from '../api/orbit';
import { showToast } from '../utils/ui/toast';
import i18n from '../i18n';
import { pushOrbitEvent } from '../utils/orbitDiag';
import { useOrbitOutboxHeartbeat } from './useOrbitOutboxHeartbeat';
} from '@/features/orbit/api/orbit';
import { showToast } from '@/utils/ui/toast';
import i18n from '@/i18n';
import { pushOrbitEvent } from '@/features/orbit/utils/orbitDiag';
import { useOrbitOutboxHeartbeat } from '@/features/orbit/hooks/useOrbitOutboxHeartbeat';
/**
* Orbit host-side tick hook.
@@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { writeOrbitHeartbeat } from '../utils/orbit';
import { orbitOutboxPlaylistName } from '../api/orbit';
import { writeOrbitHeartbeat } from '@/features/orbit/utils/orbit';
import { orbitOutboxPlaylistName } from '@/features/orbit/api/orbit';
const HEARTBEAT_TICK_MS = 10_000;
@@ -1,13 +1,13 @@
import { useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useOrbitStore } from '../store/orbitStore';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import {
suggestOrbitTrack,
hostEnqueueToOrbit,
evaluateOrbitSuggestGate,
OrbitSuggestBlockedError,
} from '../utils/orbit';
import { showToast } from '../utils/ui/toast';
} from '@/features/orbit/utils/orbit';
import { showToast } from '@/utils/ui/toast';
/**
* Shared behaviour for song rows that in "normal mode" swallow a full list
@@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { useOrbitStore } from '../store/orbitStore';
import { usePlaybackRateStore } from '../store/playbackRateStore';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { usePlaybackRateStore } from '@/store/playbackRateStore';
/** Re-sync playback rate when Orbit enters or leaves shared playback. */
export function usePlaybackRateOrbitSync() {
+53
View File
@@ -0,0 +1,53 @@
/**
* Orbit feature — multi-user listen-together: the shared-session state types
* and Navidrome-playlist transport (`api/orbit`), host/guest lifecycle +
* moderation + outbox sweep + drift math (`utils/*`), the orbit stores, the
* session/account hooks, and all Orbit UI (session bar, modals, popovers,
* guest queue, wordmark). Replaces the former `utils/orbit.ts` re-export shim.
*
* Playback-core (`playTrackAction`, `nextAction`, `queueMutationActions`,
* `resumeAction`, `playbackRateStore`, `playbackReportSession`, `previewStore`)
* drives orbit and so consumes this barrel — the correct playback→orbit edge,
* realized early (playback moves last in M3). `nextActionOrbitRadio.test.ts`
* stays out: it tests `nextAction` (playback-core), not orbit.
*/
export * from './api/orbit';
export * from './hooks/useOrbitBodyAttrs';
export * from './hooks/useOrbitGuest';
export * from './hooks/useOrbitHost';
export * from './hooks/useOrbitOutboxHeartbeat';
export * from './hooks/useOrbitSongRowBehavior';
export * from './hooks/usePlaybackRateOrbitSync';
export * from './store/orbitAccountPickerStore';
export * from './store/orbitSession';
export * from './store/orbitStore';
export * from './utils/cleanup';
export * from './utils/constants';
export * from './utils/guest';
export * from './utils/helpers';
export * from './utils/host';
export * from './utils/moderation';
export * from './utils/orbitBulkGuard';
export * from './utils/orbitDiag';
export * from './utils/orbitNames';
export * from './utils/pendingResend';
export * from './utils/remote';
export * from './utils/sessionActive';
export * from './utils/shareLink';
export * from './utils/stateMath';
export * from './utils/sweep';
export * from './utils/transitions';
export { default as OrbitAccountPicker } from './components/OrbitAccountPicker';
export { default as OrbitDiagnosticsPopover } from './components/OrbitDiagnosticsPopover';
export { default as OrbitExitModal } from './components/OrbitExitModal';
export { default as OrbitGuestQueue } from './components/OrbitGuestQueue';
export { default as OrbitHelpModal } from './components/OrbitHelpModal';
export { default as OrbitJoinModal } from './components/OrbitJoinModal';
export { default as OrbitParticipantsPopover } from './components/OrbitParticipantsPopover';
export { default as OrbitQueueHead } from './components/OrbitQueueHead';
export { default as OrbitSessionBar } from './components/OrbitSessionBar';
export { default as OrbitSettingsPopover } from './components/OrbitSettingsPopover';
export { default as OrbitSharePopover } from './components/OrbitSharePopover';
export { default as OrbitStartModal } from './components/OrbitStartModal';
export { default as OrbitStartTrigger } from './components/OrbitStartTrigger';
export { default as OrbitWordmark } from './components/OrbitWordmark';
@@ -1,4 +1,4 @@
import type { ServerProfile } from './authStoreTypes';
import type { ServerProfile } from '@/store/authStoreTypes';
import { create } from 'zustand';
let _resolve: ((server: ServerProfile | null) => void) | null = null;
@@ -7,11 +7,11 @@ const { orbitState } = vi.hoisted(() => ({
},
}));
vi.mock('./orbitStore', () => ({
vi.mock('@/features/orbit/store/orbitStore', () => ({
useOrbitStore: { getState: () => orbitState },
}));
import { isInOrbitSession } from './orbitSession';
import { isInOrbitSession } from '@/features/orbit/store/orbitSession';
beforeEach(() => {
orbitState.role = null;
@@ -1,4 +1,4 @@
import { useOrbitStore } from './orbitStore';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
/**
* True when the user is part of an Orbit session (any role, any phase short
@@ -1,5 +1,5 @@
import { create } from 'zustand';
import type { OrbitState } from '../api/orbit';
import type { OrbitState } from '@/features/orbit/api/orbit';
/**
* Orbit local session store.
@@ -6,8 +6,8 @@ import {
orbitOutboxPlaylistName,
orbitSessionPlaylistName,
type OrbitState,
} from '../../api/orbit';
import { ORBIT_ORPHAN_TTL_MS } from './constants';
} from '@/features/orbit/api/orbit';
import { ORBIT_ORPHAN_TTL_MS } from '@/features/orbit/utils/constants';
const { getPlaylists, deletePlaylist } = vi.hoisted(() => ({
getPlaylists: vi.fn(),
@@ -19,19 +19,19 @@ const { authState, orbitState } = vi.hoisted(() => ({
orbitState: { sessionId: null as string | null },
}));
vi.mock('../../api/subsonicPlaylists', () => ({ getPlaylists, deletePlaylist }));
vi.mock('../../store/authStore', () => ({
vi.mock('@/api/subsonicPlaylists', () => ({ getPlaylists, deletePlaylist }));
vi.mock('@/store/authStore', () => ({
useAuthStore: {
getState: () => ({
getActiveServer: () => (authState.username ? { username: authState.username } : undefined),
}),
},
}));
vi.mock('../../store/orbitStore', () => ({
vi.mock('@/features/orbit/store/orbitStore', () => ({
useOrbitStore: { getState: () => ({ sessionId: orbitState.sessionId }) },
}));
import { cleanupOrphanedOrbitPlaylists } from './cleanup';
import { cleanupOrphanedOrbitPlaylists } from '@/features/orbit/utils/cleanup';
type FakePlaylist = {
id: string;
@@ -1,8 +1,8 @@
import { deletePlaylist, getPlaylists } from '../../api/subsonicPlaylists';
import { useAuthStore } from '../../store/authStore';
import { useOrbitStore } from '../../store/orbitStore';
import { ORBIT_PLAYLIST_PREFIX, parseOrbitState } from '../../api/orbit';
import { ORBIT_ORPHAN_TTL_MS } from './constants';
import { deletePlaylist, getPlaylists } from '@/api/subsonicPlaylists';
import { useAuthStore } from '@/store/authStore';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { ORBIT_PLAYLIST_PREFIX, parseOrbitState } from '@/features/orbit/api/orbit';
import { ORBIT_ORPHAN_TTL_MS } from '@/features/orbit/utils/constants';
/**
* App-start sweep: delete our own __psyorbit_* playlists that no longer
@@ -1,17 +1,17 @@
import { createPlaylist, deletePlaylist, getPlaylist, getPlaylists, updatePlaylist } from '../../api/subsonicPlaylists';
import { getSong } from '../../api/subsonicLibrary';
import { songToTrack } from '../playback/songToTrack';
import { useAuthStore } from '../../store/authStore';
import { useOrbitStore } from '../../store/orbitStore';
import { usePlayerStore } from '../../store/playerStore';
import { createPlaylist, deletePlaylist, getPlaylist, getPlaylists, updatePlaylist } from '@/api/subsonicPlaylists';
import { getSong } from '@/api/subsonicLibrary';
import { songToTrack } from '@/utils/playback/songToTrack';
import { useAuthStore } from '@/store/authStore';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { usePlayerStore } from '@/store/playerStore';
import {
orbitOutboxPlaylistName,
type OrbitQueueItem,
type OrbitState,
} from '../../api/orbit';
import { suggestionKey } from './helpers';
import { notePendingSuggestion } from './pendingResend';
import { findSessionPlaylistId, readOrbitState, writeOrbitHeartbeat } from './remote';
} from '@/features/orbit/api/orbit';
import { suggestionKey } from '@/features/orbit/utils/helpers';
import { notePendingSuggestion } from '@/features/orbit/utils/pendingResend';
import { findSessionPlaylistId, readOrbitState, writeOrbitHeartbeat } from '@/features/orbit/utils/remote';
export class OrbitJoinError extends Error {
constructor(
@@ -6,13 +6,13 @@ import {
parseOrbitState,
type OrbitQueueItem,
type OrbitState,
} from '../../api/orbit';
} from '@/features/orbit/api/orbit';
import {
makeCoalescedRunner,
OrbitStateTooLarge,
serialiseOrbitState,
serialiseOrbitStateForWire,
} from './helpers';
} from '@/features/orbit/utils/helpers';
function baseState(): OrbitState {
return makeInitialOrbitState({ sid: 'aaaa1111', host: 'host', name: 'sesh' });
@@ -4,7 +4,7 @@ import {
type OrbitOutboxMeta,
type OrbitQueueItem,
type OrbitState,
} from '../../api/orbit';
} from '@/features/orbit/api/orbit';
/** 8 lowercase hex chars — unique enough for concurrent-session collision-free naming. */
export function generateSessionId(): string {
@@ -5,7 +5,7 @@ import {
ORBIT_DEFAULT_SETTINGS,
type OrbitSettings,
type OrbitState,
} from '../../api/orbit';
} from '@/features/orbit/api/orbit';
const { writeOrbitState } = vi.hoisted(() => ({ writeOrbitState: vi.fn(() => Promise.resolve()) }));
const { orbitStore } = vi.hoisted(() => ({
@@ -17,18 +17,18 @@ const { orbitStore } = vi.hoisted(() => ({
},
}));
vi.mock('./remote', () => ({
vi.mock('@/features/orbit/utils/remote', () => ({
writeOrbitState,
writeOrbitHeartbeat: vi.fn(() => Promise.resolve()),
}));
vi.mock('../../store/orbitStore', () => ({ useOrbitStore: { getState: () => orbitStore } }));
vi.mock('../../store/authStore', () => ({ useAuthStore: { getState: () => ({}) } }));
vi.mock('../../store/playerStore', () => ({ usePlayerStore: { getState: () => ({ enqueue: vi.fn() }) } }));
vi.mock('../../api/subsonicPlaylists', () => ({ createPlaylist: vi.fn(), deletePlaylist: vi.fn() }));
vi.mock('../../api/subsonicLibrary', () => ({ getSong: vi.fn() }));
vi.mock('../playback/songToTrack', () => ({ songToTrack: vi.fn() }));
vi.mock('@/features/orbit/store/orbitStore', () => ({ useOrbitStore: { getState: () => orbitStore } }));
vi.mock('@/store/authStore', () => ({ useAuthStore: { getState: () => ({}) } }));
vi.mock('@/store/playerStore', () => ({ usePlayerStore: { getState: () => ({ enqueue: vi.fn() }) } }));
vi.mock('@/api/subsonicPlaylists', () => ({ createPlaylist: vi.fn(), deletePlaylist: vi.fn() }));
vi.mock('@/api/subsonicLibrary', () => ({ getSong: vi.fn() }));
vi.mock('@/utils/playback/songToTrack', () => ({ songToTrack: vi.fn() }));
import { updateOrbitSettings } from './host';
import { updateOrbitSettings } from '@/features/orbit/utils/host';
function hostStateWith(settings: OrbitSettings | undefined): OrbitState {
const base = makeInitialOrbitState({ sid: 'aaaa1111', host: 'host', name: 'sesh' });
@@ -1,9 +1,9 @@
import { createPlaylist, deletePlaylist } from '../../api/subsonicPlaylists';
import { getSong } from '../../api/subsonicLibrary';
import { songToTrack } from '../playback/songToTrack';
import { useAuthStore } from '../../store/authStore';
import { useOrbitStore } from '../../store/orbitStore';
import { usePlayerStore } from '../../store/playerStore';
import { createPlaylist, deletePlaylist } from '@/api/subsonicPlaylists';
import { getSong } from '@/api/subsonicLibrary';
import { songToTrack } from '@/utils/playback/songToTrack';
import { useAuthStore } from '@/store/authStore';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { usePlayerStore } from '@/store/playerStore';
import {
makeInitialOrbitState,
orbitOutboxPlaylistName,
@@ -13,10 +13,10 @@ import {
type OrbitQueueItem,
type OrbitSettings,
type OrbitState,
} from '../../api/orbit';
import { generateSessionId } from './helpers';
import { readOrbitTransitionSettings } from './transitions';
import { writeOrbitHeartbeat, writeOrbitState } from './remote';
} from '@/features/orbit/api/orbit';
import { generateSessionId } from '@/features/orbit/utils/helpers';
import { readOrbitTransitionSettings } from '@/features/orbit/utils/transitions';
import { writeOrbitHeartbeat, writeOrbitState } from '@/features/orbit/utils/remote';
export interface StartOrbitArgs {
/** Human-readable name the host chose. */
@@ -1,7 +1,7 @@
import { deletePlaylist, getPlaylists } from '../../api/subsonicPlaylists';
import { useOrbitStore } from '../../store/orbitStore';
import { orbitOutboxPlaylistName, type OrbitState } from '../../api/orbit';
import { writeOrbitState } from './remote';
import { deletePlaylist, getPlaylists } from '@/api/subsonicPlaylists';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { orbitOutboxPlaylistName, type OrbitState } from '@/features/orbit/api/orbit';
import { writeOrbitState } from '@/features/orbit/utils/remote';
/**
* Host: kick a participant by username.
@@ -1,7 +1,7 @@
/**
* Orbit multi-user listen-together feature.
*
* This file is a re-export shim. Implementation lives under `utils/orbit/`:
* Orbit utils aggregator re-exports every public symbol from the sibling
* `utils/*` modules under one specifier. Orbit's own components/hooks import
* from here; the rest of the app goes through the feature barrel (`index.ts`).
*
* - `constants.ts` cadence + TTL ms values.
* - `helpers.ts` pure utilities (id gen, serialisation, key fns).
@@ -14,9 +14,6 @@
* approve/decline reactions.
* - `sweep.ts` host-side outbox sweep loop.
* - `cleanup.ts` app-start orphan playlist sweep.
*
* Importers everywhere else in the codebase keep using `'../utils/orbit'`;
* this shim re-exports every public symbol unchanged.
*/
export {
@@ -24,39 +21,39 @@ export {
ORBIT_ORPHAN_TTL_MS,
ORBIT_REMOVED_TTL_MS,
ORBIT_SHUFFLE_INTERVAL_MS,
} from './orbit/constants';
} from './constants';
export {
generateSessionId,
makeCoalescedRunner,
OrbitStateTooLarge,
serialiseOrbitState,
suggestionKey,
} from './orbit/helpers';
export { isOrbitPlaybackSyncActive } from './orbit/sessionActive';
} from './helpers';
export { isOrbitPlaybackSyncActive } from './sessionActive';
export {
applyOutboxSnapshotsToState,
computeOrbitDriftMs,
effectiveShuffleIntervalMs,
maybeShuffleQueue,
patchOrbitState,
} from './orbit/stateMath';
} from './stateMath';
export {
findSessionPlaylistId,
readOrbitState,
writeOrbitHeartbeat,
writeOrbitState,
} from './orbit/remote';
} from './remote';
export {
readOrbitTransitionSettings,
applyOrbitTransitionSettings,
saveGuestTransitionsOnce,
restoreGuestTransitions,
} from './orbit/transitions';
} from './transitions';
export {
buildOrbitShareLink,
parseOrbitShareLink,
type OrbitShareLink,
} from './orbit/shareLink';
} from './shareLink';
export {
endOrbitSession,
hostEnqueueToOrbit,
@@ -64,12 +61,12 @@ export {
triggerOrbitShuffleNow,
updateOrbitSettings,
type StartOrbitArgs,
} from './orbit/host';
} from './host';
export {
kickOrbitParticipant,
removeOrbitParticipant,
setOrbitSuggestionBlocked,
} from './orbit/moderation';
} from './moderation';
export {
approveOrbitSuggestion,
declineOrbitSuggestion,
@@ -81,11 +78,11 @@ export {
OrbitSuggestBlockedError,
suggestOrbitTrack,
type OrbitSuggestGateReason,
} from './orbit/guest';
} from './guest';
export {
forgetPendingSuggestion,
planPendingResends,
resetPendingResendState,
} from './orbit/pendingResend';
export { sweepGuestOutboxes } from './orbit/sweep';
export { cleanupOrphanedOrbitPlaylists } from './orbit/cleanup';
} from './pendingResend';
export { sweepGuestOutboxes } from './sweep';
export { cleanupOrphanedOrbitPlaylists } from './cleanup';
@@ -1,6 +1,6 @@
import { useOrbitStore } from '../store/orbitStore';
import { useConfirmModalStore } from '../store/confirmModalStore';
import i18n from '../i18n';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { useConfirmModalStore } from '@/store/confirmModalStore';
import i18n from '@/i18n';
/**
* Ask the user before dropping many tracks into the shared Orbit queue.
@@ -11,7 +11,7 @@
* for power users who want a persistent file.
*/
import { invoke } from '@tauri-apps/api/core';
import { useAuthStore } from '../store/authStore';
import { useAuthStore } from '@/store/authStore';
/** Hard cap so a long session doesn't spend memory unbounded. ~200 entries. */
const MAX_EVENTS = 200;
@@ -8,7 +8,7 @@ import {
pendingResendTrackedCount,
planPendingResends,
resetPendingResendState,
} from './pendingResend';
} from '@/features/orbit/utils/pendingResend';
const T0 = 1_000_000;
const GRACE = ORBIT_SUGGESTION_RESEND_GRACE_MS;
@@ -1,11 +1,11 @@
import { getPlaylist, getPlaylists, updatePlaylistMeta } from '../../api/subsonicPlaylists';
import { getPlaylist, getPlaylists, updatePlaylistMeta } from '@/api/subsonicPlaylists';
import {
orbitSessionPlaylistName,
parseOrbitState,
type OrbitOutboxMeta,
type OrbitState,
} from '../../api/orbit';
import { serialiseOrbitStateForWire, serialiseOutboxMeta } from './helpers';
} from '@/features/orbit/api/orbit';
import { serialiseOrbitStateForWire, serialiseOutboxMeta } from '@/features/orbit/utils/helpers';
/** Pull + parse the canonical state from the session playlist. Null on miss or parse error. */
export async function readOrbitState(sessionPlaylistId: string): Promise<OrbitState | null> {
@@ -1,5 +1,5 @@
import type { OrbitPhase, OrbitRole } from '../../store/orbitStore';
import { useOrbitStore } from '../../store/orbitStore';
import type { OrbitPhase, OrbitRole } from '@/features/orbit/store/orbitStore';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
/**
* True while this client is in an Orbit session on the shared playback path
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { buildOrbitShareLink, parseOrbitShareLink } from './shareLink';
import { buildOrbitShareLink, parseOrbitShareLink } from '@/features/orbit/utils/shareLink';
describe('parseOrbitShareLink', () => {
it('round-trips an https invite', () => {
@@ -1,4 +1,4 @@
import { decodeOrbitSharePayloadFromText, encodeSharePayload } from '../share/shareLink';
import { decodeOrbitSharePayloadFromText, encodeSharePayload } from '@/utils/share/shareLink';
export interface OrbitShareLink {
/** Base URL of the Navidrome server (decoded). */
@@ -1,12 +1,12 @@
import { describe, expect, it, vi } from 'vitest';
vi.mock('../../store/orbitStore', () => ({
vi.mock('@/features/orbit/store/orbitStore', () => ({
useOrbitStore: { getState: () => ({ state: null, setState: vi.fn() }) },
}));
import { makeInitialOrbitState, type OrbitQueueItem, type OrbitState } from '../../api/orbit';
import { ORBIT_QUEUE_HISTORY_LIMIT } from './constants';
import { applyOutboxSnapshotsToState, type OutboxSnapshot } from './stateMath';
import { makeInitialOrbitState, type OrbitQueueItem, type OrbitState } from '@/features/orbit/api/orbit';
import { ORBIT_QUEUE_HISTORY_LIMIT } from '@/features/orbit/utils/constants';
import { applyOutboxSnapshotsToState, type OutboxSnapshot } from '@/features/orbit/utils/stateMath';
function stateWithQueue(queue: OrbitQueueItem[]): OrbitState {
return { ...makeInitialOrbitState({ sid: 'aaaa1111', host: 'host', name: 'sesh' }), queue };
@@ -1,15 +1,15 @@
import { useOrbitStore } from '../../store/orbitStore';
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import type {
OrbitParticipant,
OrbitQueueItem,
OrbitState,
} from '../../api/orbit';
} from '@/features/orbit/api/orbit';
import {
ORBIT_HEARTBEAT_ALIVE_MS,
ORBIT_QUEUE_HISTORY_LIMIT,
ORBIT_REMOVED_TTL_MS,
ORBIT_SHUFFLE_INTERVAL_MS,
} from './constants';
} from '@/features/orbit/utils/constants';
/**
* Keep `OrbitState.queue` bounded. Drops the oldest suggestions (by `addedAt`)
@@ -1,7 +1,7 @@
import { getPlaylist, getPlaylists, updatePlaylist } from '../../api/subsonicPlaylists';
import { type OrbitOutboxMeta } from '../../api/orbit';
import { parseOutboxPlaylistName } from './helpers';
import { type OutboxSnapshot } from './stateMath';
import { getPlaylist, getPlaylists, updatePlaylist } from '@/api/subsonicPlaylists';
import { type OrbitOutboxMeta } from '@/features/orbit/api/orbit';
import { parseOutboxPlaylistName } from '@/features/orbit/utils/helpers';
import { type OutboxSnapshot } from '@/features/orbit/utils/stateMath';
/**
* Host: list all guest outbox playlists for the current session.
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { OrbitTransitionSettings } from '../../api/orbit';
import type { OrbitTransitionSettings } from '@/features/orbit/api/orbit';
const { store, setState } = vi.hoisted(() => {
const store = {
@@ -20,7 +20,7 @@ const { store, setState } = vi.hoisted(() => {
return { store, setState };
});
vi.mock('../../store/authStore', () => ({
vi.mock('@/store/authStore', () => ({
useAuthStore: { getState: () => store.state, setState },
}));
@@ -30,7 +30,7 @@ import {
readOrbitTransitionSettings,
restoreGuestTransitions,
saveGuestTransitionsOnce,
} from './transitions';
} from '@/features/orbit/utils/transitions';
const GUEST_OWN: OrbitTransitionSettings = {
crossfadeEnabled: false,
@@ -1,9 +1,9 @@
import { useAuthStore } from '../../store/authStore';
import type { OrbitTransitionSettings } from '../../api/orbit';
import { useAuthStore } from '@/store/authStore';
import type { OrbitTransitionSettings } from '@/features/orbit/api/orbit';
import {
sanitizeAutodjOverlapCapMode,
sanitizeAutodjOverlapCapSec,
} from '../playback/autodjOverlapCap';
} from '@/utils/playback/autodjOverlapCap';
/**
* Bridge between the local playback-transition settings (in `authStore`) and
@@ -19,9 +19,9 @@ import {
type PlaybackStrategy,
} from '@/utils/audio/playbackRateHelpers';
import { usePlaybackRateStore } from '@/store/playbackRateStore';
import { useOrbitStore } from '@/store/orbitStore';
import { useOrbitStore } from '@/features/orbit';
import { useAuthStore } from '@/store/authStore';
import { isOrbitPlaybackSyncActive } from '@/utils/orbit';
import { isOrbitPlaybackSyncActive } from '@/features/orbit';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
import { SettingsSubCard } from '@/features/settings/components/SettingsSubCard';
@@ -1,7 +1,7 @@
import React from 'react';
import type { TFunction } from 'i18next';
import { useAuthStore } from '@/store/authStore';
import { useOrbitStore } from '@/store/orbitStore';
import { useOrbitStore } from '@/features/orbit';
import {
AUTODJ_OVERLAP_CAP_MAX_SEC,
AUTODJ_OVERLAP_CAP_MIN_SEC,
@@ -9,8 +9,8 @@ import { playbackReportStopped } from '../../store/playbackReportSession';
import { getPlaybackProgressSnapshot } from '../../store/playbackProgress';
import { usePlayerStore } from '../../store/playerStore';
import { useAuthStore } from '../../store/authStore';
import { useOrbitStore } from '../../store/orbitStore';
import { endOrbitSession, leaveOrbitSession } from '../../utils/orbit';
import { useOrbitStore } from '@/features/orbit';
import { endOrbitSession, leaveOrbitSession } from '@/features/orbit';
import {
canRunShortcutActionInMiniWindow,
executeRuntimeAction,
+1 -1
View File
@@ -1,7 +1,7 @@
import { useEffect, useRef } from 'react';
import { applyServerPlayQueue } from '../store/applyServerPlayQueue';
import { useAuthStore } from '../store/authStore';
import { useOrbitStore } from '../store/orbitStore';
import { useOrbitStore } from '@/features/orbit';
import { usePlayerStore } from '../store/playerStore';
import {
getPlaybackIdleSinceMs,
+1 -1
View File
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import type { ConnectionStatus } from '../hooks/useConnectionStatus';
import { pullPlayQueueFromActiveServer } from '../store/applyServerPlayQueue';
import { useAuthStore } from '../store/authStore';
import { useOrbitStore } from '../store/orbitStore';
import { useOrbitStore } from '@/features/orbit';
import { usePlayerStore } from '../store/playerStore';
import { getPlaybackServerId, queueIsMultiServer } from '../utils/playback/playbackServer';
import {
+1 -1
View File
@@ -2,7 +2,7 @@ import { useEffect } from 'react';
import { getMusicFolders } from '../api/subsonicLibrary';
import { probeEntityRatingSupport } from '../api/subsonicStarRating';
import { useAuthStore } from '../store/authStore';
import { cleanupOrphanedOrbitPlaylists } from '../utils/orbit';
import { cleanupOrphanedOrbitPlaylists } from '@/features/orbit';
/**
* Per-server one-shot probe run after login:
+1 -1
View File
@@ -11,7 +11,7 @@ import { useParams, useSearchParams } from 'react-router-dom';
import { invoke } from '@tauri-apps/api/core';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
import { useOrbitSongRowBehavior } from '../hooks/useOrbitSongRowBehavior';
import { useOrbitSongRowBehavior } from '@/features/orbit';
import { useAlbumDetailData } from '../hooks/useAlbumDetailData';
import { useAlbumOfflineState } from '../hooks/useAlbumOfflineState';
import { useAlbumDetailSort } from '../hooks/useAlbumDetailSort';
+1 -1
View File
@@ -8,7 +8,7 @@ import { useAuthStore } from '../store/authStore';
import { useLibraryIndexStore } from '../store/libraryIndexStore';
import { useTranslation } from 'react-i18next';
import { useIsMobile } from '../hooks/useIsMobile';
import { useOrbitSongRowBehavior } from '../hooks/useOrbitSongRowBehavior';
import { useOrbitSongRowBehavior } from '@/features/orbit';
import {
fetchRandomMixSongsUntilFull,
getMixMinRatingsConfigFromAuth,
+1 -1
View File
@@ -9,7 +9,7 @@ import {
isInfiniteQueueFetching,
setInfiniteQueueFetching,
} from './infiniteQueueState';
import { isInOrbitSession } from './orbitSession';
import { isInOrbitSession } from '@/features/orbit';
import type { PlayerState, QueueItemRef, Track } from './playerStoreTypes';
import { toQueueItemRefs } from '../utils/library/queueItemRef';
import { resolveQueueTrack } from '../utils/library/queueTrackView';
+1 -1
View File
@@ -9,7 +9,7 @@ const { inOrbit, getSimilarSongs2, getTopSongs } = vi.hoisted(() => ({
vi.mock('../api/subsonicArtists', () => ({ getSimilarSongs2, getTopSongs }));
vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn(() => Promise.resolve()) }));
vi.mock('./orbitSession', () => ({ isInOrbitSession: () => inOrbit.value }));
vi.mock('@/features/orbit', () => ({ isInOrbitSession: () => inOrbit.value }));
vi.mock('./authStore', () => ({
useAuthStore: { getState: () => ({ infiniteQueueEnabled: false }) },
}));
+2 -2
View File
@@ -2,7 +2,7 @@ import { playbackReportStart, playbackReportStopped } from './playbackReportSess
import { invoke } from '@tauri-apps/api/core';
import { getMusicNetworkRuntimeOrNull } from '../music-network';
import { setDeferHotCachePrefetch } from '../utils/cache/hotCacheGate';
import { orbitBulkGuard } from '../utils/orbitBulkGuard';
import { orbitBulkGuard } from '@/features/orbit';
import { sameQueueTrackId } from '../utils/playback/queueIdentity';
import {
computeAutodjManualBlendPlan,
@@ -53,7 +53,7 @@ import {
import { refreshLoudnessForTrack } from './loudnessRefresh';
import { fetchWaveformBins, refreshWaveformForTrack } from '@/store/waveformRefresh';
import { deriveNormalizationSnapshot } from './normalizationSnapshot';
import { useOrbitStore } from './orbitStore';
import { useOrbitStore } from '@/features/orbit';
import {
playbackSourceHintForResolvedUrl,
recordEnginePlayUrl,
+1 -1
View File
@@ -14,7 +14,7 @@ import {
shouldRestartPlaybackForRateChange,
type PlaybackRateSnapshot,
} from '../utils/audio/playbackRateRestart';
import { isOrbitPlaybackSyncActive } from '../utils/orbit';
import { isOrbitPlaybackSyncActive } from '@/features/orbit';
interface PlaybackRateState extends PlaybackRateSnapshot {
/** UI-only: smaller slider steps (Advanced). Not sent to the engine. */
+1 -1
View File
@@ -19,7 +19,7 @@ vi.mock('./playbackRateStore', () => ({
},
}));
vi.mock('../utils/audio/playbackRateHelpers', () => ({ isPlaybackRateApplied: () => false }));
vi.mock('../utils/orbit', () => ({ isOrbitPlaybackSyncActive: () => false }));
vi.mock('@/features/orbit', () => ({ isOrbitPlaybackSyncActive: () => false }));
import { reportNowPlaying, reportPlayback } from '../api/subsonicScrobble';
import { isFeatureActiveForServer } from '../serverCapabilities/storeView';
+1 -1
View File
@@ -3,7 +3,7 @@ import type { PlaybackReportState } from '../api/subsonicTypes';
import { FEATURE_PLAYBACK_REPORT } from '../serverCapabilities/catalog';
import { isFeatureActiveForServer } from '../serverCapabilities/storeView';
import { isPlaybackRateApplied } from '../utils/audio/playbackRateHelpers';
import { isOrbitPlaybackSyncActive } from '../utils/orbit';
import { isOrbitPlaybackSyncActive } from '@/features/orbit';
import { useAuthStore } from './authStore';
import { getPlaybackProgressSnapshot } from './playbackProgress';
import { usePlaybackRateStore } from './playbackRateStore';
+1 -1
View File
@@ -44,7 +44,7 @@ vi.mock('@/music-network', () => {
};
});
vi.mock('@/utils/orbitBulkGuard', () => ({
vi.mock('@/features/orbit', () => ({
orbitBulkGuard: vi.fn(async () => true),
}));
+1 -1
View File
@@ -42,7 +42,7 @@ vi.mock('@/music-network', () => ({
getMusicNetworkRuntimeOrNull: () => runtimeMock,
}));
vi.mock('@/utils/orbitBulkGuard', () => ({
vi.mock('@/features/orbit', () => ({
orbitBulkGuard: vi.fn(async () => true),
}));
@@ -47,7 +47,7 @@ vi.mock('@/music-network', () => {
};
});
vi.mock('@/utils/orbitBulkGuard', () => ({
vi.mock('@/features/orbit', () => ({
orbitBulkGuard: vi.fn(async () => true),
}));
+1 -1
View File
@@ -26,7 +26,7 @@ vi.mock('@/api/subsonic', async () => {
// `enqueue` / `enqueueAt` call `orbitBulkGuard` for multi-track inserts when
// the caller hasn't pre-confirmed. Force the guard to short-circuit through.
vi.mock('@/utils/orbitBulkGuard', () => ({
vi.mock('@/features/orbit', () => ({
orbitBulkGuard: vi.fn(async () => true),
}));
+1 -1
View File
@@ -28,7 +28,7 @@ vi.mock('@/api/subsonic', () => ({
import { usePreviewStore } from './previewStore';
import { useAuthStore } from './authStore';
import { useOrbitStore } from './orbitStore';
import { useOrbitStore } from '@/features/orbit';
import { usePlayerStore } from './playerStore';
import './previewPlayerVolumeSync';
import { onInvoke, invokeMock } from '@/test/mocks/tauri';
+1 -1
View File
@@ -5,7 +5,7 @@ import { create } from 'zustand';
import { invoke } from '@tauri-apps/api/core';
import { usePlayerStore } from './playerStore';
import { useAuthStore } from './authStore';
import { isOrbitPlaybackSyncActive } from '../utils/orbit';
import { isOrbitPlaybackSyncActive } from '@/features/orbit';
/** Minimal track info needed to surface the preview in the player bar UI. */
export interface PreviewingTrack {
+1 -1
View File
@@ -1,5 +1,5 @@
import { invoke } from '@tauri-apps/api/core';
import { orbitBulkGuard } from '../utils/orbitBulkGuard';
import { orbitBulkGuard } from '@/features/orbit';
import { useAuthStore } from './authStore';
import { setIsAudioPaused } from './engineState';
import { prefetchLoudnessForEnqueuedTracks } from './loudnessPrefetch';
+2 -2
View File
@@ -1,6 +1,6 @@
import { getSong } from '../api/subsonicLibrary';
import { invoke } from '@tauri-apps/api/core';
import { estimateLivePosition } from '../api/orbit';
import { estimateLivePosition } from '@/features/orbit';
import { setDeferHotCachePrefetch } from '../utils/cache/hotCacheGate';
import {
getPlaybackCacheServerKey,
@@ -22,7 +22,7 @@ import {
isReplayGainActive,
loudnessGainDbForEngineBind,
} from './loudnessGainCache';
import { useOrbitStore } from './orbitStore';
import { useOrbitStore } from '@/features/orbit';
import {
playbackSourceHintForResolvedUrl,
recordEnginePlayUrl,
+1 -1
View File
@@ -25,7 +25,7 @@ import { _resetTimelineSessionHistoryForTest } from '@/store/timelineSessionHist
import { usePlayerStore } from '@/store/playerStore';
import { useAuthStore } from '@/store/authStore';
import { usePreviewStore } from '@/store/previewStore';
import { useOrbitStore } from '@/store/orbitStore';
import { useOrbitStore } from '@/features/orbit';
const INITIAL_PLAYER_STATE = usePlayerStore.getState();
const INITIAL_AUTH_STATE = useAuthStore.getState();
+1 -1
View File
@@ -13,7 +13,7 @@ vi.mock('./fadeOut', () => ({
}));
import { resolveAlbumForActiveServer } from '@/features/offline';
import { useOrbitStore } from '../../store/orbitStore';
import { useOrbitStore } from '@/features/orbit';
import { usePlayerStore } from '../../store/playerStore';
import { playAlbum, playAlbumShuffled } from './playAlbum';
import * as shuffleModule from './shuffleArray';
+1 -1
View File
@@ -1,7 +1,7 @@
import { usePlayerStore } from '../../store/playerStore';
import { resolveAlbumForActiveServer } from '@/features/offline';
import { songToTrack } from './songToTrack';
import { useOrbitStore } from '../../store/orbitStore';
import { useOrbitStore } from '@/features/orbit';
import { fadeOut } from './fadeOut';
import { shouldAutodjInterruptBlend } from './autodjManualBlend';
import type { Track } from '../../store/playerStoreTypes';
+2 -2
View File
@@ -5,10 +5,10 @@ import {
coverTrafficEndServerSwitch,
} from '../../cover/coverTraffic';
import { useAuthStore } from '../../store/authStore';
import { useOrbitStore } from '../../store/orbitStore';
import { useOrbitStore } from '@/features/orbit';
import { flushPlayQueueForServer } from '../../store/queueSync';
import { markQueueHandoffPending } from '../../store/queueSyncUiState';
import { endOrbitSession, leaveOrbitSession } from '../orbit';
import { endOrbitSession, leaveOrbitSession } from '@/features/orbit';
import { ensureConnectUrlResolved } from './serverEndpoint';
import { syncServerHttpContextForProfile } from './syncServerHttpContext';
@@ -53,7 +53,7 @@ vi.mock('../playback/songToTrack', () => ({
songToTrack: mocks.songToTrack,
}));
vi.mock('../orbitBulkGuard', () => ({
vi.mock('@/features/orbit', () => ({
orbitBulkGuard: mocks.orbitBulkGuard,
}));
+1 -1
View File
@@ -12,7 +12,7 @@ import type { ServerProfile } from '../../store/authStoreTypes';
import { usePlayerStore } from '../../store/playerStore';
import { songToTrack } from '../playback/songToTrack';
import type { Track } from '../../store/playerStoreTypes';
import { orbitBulkGuard } from '../orbitBulkGuard';
import { orbitBulkGuard } from '@/features/orbit';
import { findServerIdForShareUrl } from './shareLink';
import { connectBaseUrlForServer } from '../server/serverEndpoint';
import { serverIndexKeyFromUrl } from '../server/serverIndexKey';