refactor(auth): E.43 — extract authStore types + defaults + helpers (#608)

First slice of the authStore split. Pure code-move: no behaviour change.

- `authStoreTypes.ts` — `ServerProfile`, `AuthState`, plus all union
  types (`SeekbarStyle`, `LoggingMode`, `NormalizationEngine`,
  `DiscordCoverSource`, `LoudnessLufsPreset`, `LyricsSourceId`,
  `LyricsSourceConfig`, `TrackPreviewLocation`, `TrackPreviewLocations`).
- `authStoreDefaults.ts` — `LOUDNESS_LUFS_PRESETS`,
  `DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB`,
  `TRACK_PREVIEW_LOCATIONS`, `DEFAULT_TRACK_PREVIEW_LOCATIONS`,
  `DEFAULT_LYRICS_SOURCES`, `MIX_MIN_RATING_FILTER_MAX_STARS`,
  `RANDOM_MIX_SIZE_OPTIONS`.
- `authStoreHelpers.ts` — `generateId`,
  `sanitizeLoudnessLufsPreset`, `sanitizeLoudnessPreAnalysisFromStorage`,
  `clampMixFilterMinStars`, `clampRandomMixSize`,
  `clampSkipStarThreshold`, `skipStarCountStorageKey`,
  `sanitizeSkipStarCounts`.

12 external call sites migrated to direct imports from the new
modules (no re-export shims left in authStore.ts — applies the
[feedback_prevent_god_modules] rule 5: avoid re-export debt).

authStore.ts: 889 → 518 LOC (−371).
This commit is contained in:
Frank Stellmacher
2026-05-12 23:22:28 +02:00
committed by GitHub
parent 9fac6eb490
commit 4d564e5016
16 changed files with 450 additions and 419 deletions
+2 -1
View File
@@ -1,10 +1,11 @@
import type { ServerProfile } from '../store/authStoreTypes';
import React, { useState, useEffect, useLayoutEffect, useRef, useCallback } from 'react'; import React, { useState, useEffect, useLayoutEffect, useRef, useCallback } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Check, ChevronDown } from 'lucide-react'; import { Check, ChevronDown } from 'lucide-react';
import { ConnectionStatus } from '../hooks/useConnectionStatus'; import { ConnectionStatus } from '../hooks/useConnectionStatus';
import { useAuthStore, type ServerProfile } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { switchActiveServer } from '../utils/switchActiveServer'; import { switchActiveServer } from '../utils/switchActiveServer';
import { showToast } from '../utils/toast'; import { showToast } from '../utils/toast';
import { serverListDisplayLabel } from '../utils/serverDisplayName'; import { serverListDisplayLabel } from '../utils/serverDisplayName';
+2 -1
View File
@@ -6,6 +6,7 @@
* snapshots — tests survive a refactor that re-orders or re-styles the * snapshots — tests survive a refactor that re-orders or re-styles the
* markup as long as the menu items + their handlers stay observable. * markup as long as the menu items + their handlers stay observable.
*/ */
import type { ServerProfile } from '@/store/authStoreTypes';
import type { Track } from '@/store/playerStoreTypes'; import type { Track } from '@/store/playerStoreTypes';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
@@ -45,7 +46,7 @@ vi.mock('@/utils/orbitBulkGuard', () => ({
import ContextMenu from './ContextMenu'; import ContextMenu from './ContextMenu';
import { renderWithProviders } from '@/test/helpers/renderWithProviders'; import { renderWithProviders } from '@/test/helpers/renderWithProviders';
import { usePlayerStore } from '@/store/playerStore'; import { usePlayerStore } from '@/store/playerStore';
import { useAuthStore, type ServerProfile } from '@/store/authStore'; import { useAuthStore } from '@/store/authStore';
import { resetAllStores } from '@/test/helpers/storeReset'; import { resetAllStores } from '@/test/helpers/storeReset';
import { makeTrack, makeServer } from '@/test/helpers/factories'; import { makeTrack, makeServer } from '@/test/helpers/factories';
import { onInvoke } from '@/test/mocks/tauri'; import { onInvoke } from '@/test/mocks/tauri';
+2 -1
View File
@@ -1,8 +1,9 @@
import type { SeekbarStyle } from '../store/authStoreTypes';
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../store/playbackProgress'; import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../store/playbackProgress';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { usePlayerStore } from '../store/playerStore'; import { usePlayerStore } from '../store/playerStore';
import { usePreviewStore } from '../store/previewStore'; import { usePreviewStore } from '../store/previewStore';
import { useAuthStore, type SeekbarStyle } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { bumpPerfCounter } from '../utils/perfTelemetry'; import { bumpPerfCounter } from '../utils/perfTelemetry';
function fmt(s: number): string { function fmt(s: number): string {
if (!s || isNaN(s)) return '0:00'; if (!s || isNaN(s)) return '0:00';
+2 -1
View File
@@ -1,9 +1,10 @@
import { RANDOM_MIX_SIZE_OPTIONS } from '../store/authStoreDefaults';
import { songToTrack } from '../utils/songToTrack'; import { songToTrack } from '../utils/songToTrack';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { getGenres, SubsonicSong, SubsonicGenre, star, unstar } from '../api/subsonic'; import { getGenres, SubsonicSong, SubsonicGenre, star, unstar } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore'; import { usePlayerStore } from '../store/playerStore';
import { usePreviewStore } from '../store/previewStore'; import { usePreviewStore } from '../store/previewStore';
import { useAuthStore, RANDOM_MIX_SIZE_OPTIONS } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { Play, RefreshCw, ChevronDown, ChevronRight, ChevronUp, Heart, Square, AudioLines } from 'lucide-react'; import { Play, RefreshCw, ChevronDown, ChevronRight, ChevronUp, Heart, Square, AudioLines } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDragDrop } from '../contexts/DragDropContext'; import { useDragDrop } from '../contexts/DragDropContext';
+3 -13
View File
@@ -1,3 +1,5 @@
import type { ServerProfile, SeekbarStyle, LyricsSourceId, LyricsSourceConfig, LoggingMode, LoudnessLufsPreset, TrackPreviewLocation } from '../store/authStoreTypes';
import { DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB, MIX_MIN_RATING_FILTER_MAX_STARS, TRACK_PREVIEW_LOCATIONS } from '../store/authStoreDefaults';
import React, { useState, useMemo, useCallback, useEffect, useLayoutEffect, useRef } from 'react'; import React, { useState, useMemo, useCallback, useEffect, useLayoutEffect, useRef } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { version as appVersion } from '../../package.json'; import { version as appVersion } from '../../package.json';
@@ -27,19 +29,7 @@ import { AboutPsysonicBrandHeader } from '../components/AboutPsysonicLol';
import { useLuckyMixAvailable } from '../hooks/useLuckyMixAvailable'; import { useLuckyMixAvailable } from '../hooks/useLuckyMixAvailable';
import ThemePicker, { THEME_GROUPS } from '../components/ThemePicker'; import ThemePicker, { THEME_GROUPS } from '../components/ThemePicker';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { import { useAuthStore } from '../store/authStore';
useAuthStore,
DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB,
ServerProfile,
MIX_MIN_RATING_FILTER_MAX_STARS,
TRACK_PREVIEW_LOCATIONS,
type SeekbarStyle,
type LyricsSourceId,
type LyricsSourceConfig,
type LoggingMode,
type LoudnessLufsPreset,
type TrackPreviewLocation,
} from '../store/authStore';
import { SeekbarPreview } from '../components/WaveformSeek'; import { SeekbarPreview } from '../components/WaveformSeek';
import { IS_LINUX, IS_MACOS, IS_WINDOWS } from '../utils/platform'; import { IS_LINUX, IS_MACOS, IS_WINDOWS } from '../utils/platform';
import { useThemeStore } from '../store/themeStore'; import { useThemeStore } from '../store/themeStore';
+21 -392
View File
@@ -1,11 +1,8 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware'; import { persist, createJSONStorage } from 'zustand/middleware';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import type { EntityRatingSupportLevel } from '../api/subsonic';
import { import {
isNavidromeAudiomuseSoftwareEligible, isNavidromeAudiomuseSoftwareEligible,
type InstantMixProbeResult,
type SubsonicServerIdentity,
} from '../utils/subsonicServerIdentity'; } from '../utils/subsonicServerIdentity';
import { usePlayerStore } from './playerStore'; import { usePlayerStore } from './playerStore';
import { IS_LINUX } from '../utils/platform'; import { IS_LINUX } from '../utils/platform';
@@ -13,397 +10,29 @@ import {
LOUDNESS_PRE_ANALYSIS_REF_TARGET_LUFS, LOUDNESS_PRE_ANALYSIS_REF_TARGET_LUFS,
clampStoredLoudnessPreAnalysisAttenuationRefDb, clampStoredLoudnessPreAnalysisAttenuationRefDb,
} from '../utils/loudnessPreAnalysisSlider'; } from '../utils/loudnessPreAnalysisSlider';
import {
export interface ServerProfile { DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB,
id: string; DEFAULT_LYRICS_SOURCES,
name: string; DEFAULT_TRACK_PREVIEW_LOCATIONS,
url: string; } from './authStoreDefaults';
username: string; import {
password: string; clampMixFilterMinStars,
} clampRandomMixSize,
clampSkipStarThreshold,
export type SeekbarStyle = 'truewave' | 'pseudowave' | 'linedot' | 'bar' | 'thick' | 'segmented' | 'neon' | 'pulsewave' | 'particletrail' | 'liquidfill' | 'retrotape'; generateId,
export type LoggingMode = 'off' | 'normal' | 'debug'; sanitizeLoudnessLufsPreset,
export type NormalizationEngine = 'off' | 'replaygain' | 'loudness'; sanitizeLoudnessPreAnalysisFromStorage,
export type DiscordCoverSource = 'none' | 'apple' | 'server'; sanitizeSkipStarCounts,
skipStarCountStorageKey,
/** Integrated-loudness target presets (Settings + analysis). */ } from './authStoreHelpers';
export type LoudnessLufsPreset = -16 | -14 | -12 | -10; import type {
AuthState,
const LOUDNESS_LUFS_PRESETS: LoudnessLufsPreset[] = [-16, -14, -12, -10]; DiscordCoverSource,
LyricsSourceConfig,
/** Settings default + Rust engine cold default until `audio_set_normalization` runs. */ SeekbarStyle,
export const DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB = -4.5; } from './authStoreTypes';
function sanitizeLoudnessLufsPreset(v: unknown, fallback: LoudnessLufsPreset): LoudnessLufsPreset {
return (LOUDNESS_LUFS_PRESETS as readonly number[]).includes(v as number)
? (v as LoudnessLufsPreset)
: fallback;
}
function sanitizeLoudnessPreAnalysisFromStorage(v: unknown): number {
const n = typeof v === 'number' ? v : Number(v);
if (!Number.isFinite(n)) return DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB;
return clampStoredLoudnessPreAnalysisAttenuationRefDb(n);
}
export type LyricsSourceId = 'server' | 'lrclib' | 'netease';
export interface LyricsSourceConfig { id: LyricsSourceId; enabled: boolean; }
export type TrackPreviewLocation =
| 'suggestions'
| 'albums'
| 'playlists'
| 'favorites'
| 'artist'
| 'randomMix';
export type TrackPreviewLocations = Record<TrackPreviewLocation, boolean>;
export const TRACK_PREVIEW_LOCATIONS: readonly TrackPreviewLocation[] = [
'suggestions',
'albums',
'playlists',
'favorites',
'artist',
'randomMix',
];
const DEFAULT_TRACK_PREVIEW_LOCATIONS: TrackPreviewLocations = {
suggestions: true,
albums: true,
playlists: true,
favorites: true,
artist: true,
randomMix: true,
};
const DEFAULT_LYRICS_SOURCES: LyricsSourceConfig[] = [
{ id: 'server', enabled: true },
{ id: 'lrclib', enabled: true },
{ id: 'netease', enabled: false },
];
interface AuthState {
// Multi-server
servers: ServerProfile[];
activeServerId: string | null;
// Last.fm (global)
lastfmApiKey: string;
lastfmApiSecret: string;
lastfmSessionKey: string;
lastfmUsername: string;
// Settings (global)
scrobblingEnabled: boolean;
maxCacheMb: number;
downloadFolder: string;
offlineDownloadDir: string;
excludeAudiobooks: boolean;
customGenreBlacklist: string[];
replayGainEnabled: boolean;
normalizationEngine: NormalizationEngine;
loudnessTargetLufs: LoudnessLufsPreset;
/**
* dB extra quieting until loudness is saved, **calibrated for 14 LUFS** target; engine applies
* `+ (loudnessTargetLufs - (14))` for other targets. See `effectiveLoudnessPreAnalysisAttenuationDb`.
*/
loudnessPreAnalysisAttenuationDb: number;
/** Persisted: stored pre is ref @ 14 (v1+); legacy falsey entries migrate once in onRehydrate. */
loudnessPreIsRefV1?: boolean;
replayGainMode: 'track' | 'album' | 'auto';
replayGainPreGainDb: number; // added to RG gain for tagged files (0…+6 dB)
replayGainFallbackDb: number; // gain for untagged files / radio (-6…0 dB)
crossfadeEnabled: boolean;
crossfadeSecs: number;
gaplessEnabled: boolean;
/** Show inline Play+Preview buttons in tracklists. Default on per Q3. Master kill switch — when off, all locations are off. */
trackPreviewsEnabled: boolean;
/** Per-location toggles. Only honoured when `trackPreviewsEnabled` is true. */
trackPreviewLocations: TrackPreviewLocations;
/** Mid-track start position as a 0…1 ratio. Default 0.33 = 33%. */
trackPreviewStartRatio: number;
/** Preview window length in seconds. Default 30 s. */
trackPreviewDurationSec: number;
preloadMode: 'off' | 'balanced' | 'early' | 'custom';
preloadCustomSeconds: number;
infiniteQueueEnabled: boolean;
preservePlayNextOrder: boolean;
showArtistImages: boolean;
showTrayIcon: boolean;
minimizeToTray: boolean;
/** Whether the "Orbit" topbar trigger is rendered. Users who never
* touch Orbit can hide it so the header stays uncluttered. */
showOrbitTrigger: boolean;
discordRichPresence: boolean;
discordCoverSource: DiscordCoverSource;
/** Opt-in: fetch upcoming tour dates from Bandsintown for the Now-Playing info panel. */
enableBandsintown: boolean;
discordTemplateDetails: string;
discordTemplateState: string;
discordTemplateLargeText: string;
useCustomTitlebar: boolean;
/** Pre-build the mini-player webview at app start on Linux/macOS so content is available instantly
* on first open. Ignored on Windows that platform always pre-creates as a hang workaround. */
preloadMiniPlayer: boolean;
/** Linux WebKitGTK: smooth wheel on when true; off only after explicit opt-out in Settings. */
linuxWebkitKineticScroll: boolean;
/** Runtime backend logging level. */
loggingMode: LoggingMode;
nowPlayingEnabled: boolean;
lyricsServerFirst: boolean;
enableNeteaselyrics: boolean;
lyricsSources: LyricsSourceConfig[];
/**
* `'standard'` server + lrclib + netease pipeline (configurable order).
* `'lyricsplus'` YouLyPlus / lyricsplus first, silent fallback to standard
* pipeline when no data is returned.
*/
lyricsMode: 'standard' | 'lyricsplus';
/**
* Render synced lines as static text (no auto-scroll, no word highlighting).
* Honoured in both lyrics modes.
*/
lyricsStaticOnly: boolean;
showFullscreenLyrics: boolean;
/** 'rail' = classic 5-line sliding rail; 'apple' = full-screen scrolling list */
fsLyricsStyle: 'rail' | 'apple';
/** Sidebar lyrics scroll style: 'classic' = scrollIntoView center; 'apple' = scroll to 35% */
sidebarLyricsStyle: 'classic' | 'apple';
showFsArtistPortrait: boolean;
/** Portrait dimming 0100 (percent), applied as CSS rgba alpha */
fsPortraitDim: number;
showChangelogOnUpdate: boolean;
lastSeenChangelogVersion: string;
seekbarStyle: SeekbarStyle;
/** Persisted UI toggle: is the Now Playing section in queue panel collapsed */
queueNowPlayingCollapsed: boolean;
/** Alpha: native hi-res sample rate output (disabled = safe 44.1 kHz mode) */
enableHiRes: boolean;
/** Selected audio output device name. null = system default. */
audioOutputDevice: string | null;
/** Alpha: ephemeral queue prefetch cache on disk */
hotCacheEnabled: boolean;
hotCacheMaxMb: number;
hotCacheDebounceSec: number;
/** Parent directory; actual cache is `<dir>/psysonic-hot-cache/`. Empty = app data. */
hotCacheDownloadDir: string;
/** After this many manual skips of the same track, set track rating to 1 if still unrated (below 1 star). */
skipStarOnManualSkipsEnabled: boolean;
/** Manual skips per track before applying rating 1 (when enabled). */
skipStarManualSkipThreshold: number;
/**
* Manual Next-count per track for skip1. Key = `${serverId}\\u001f${trackId}`
* (empty serverId when none). Persisted; cleared when the track finishes naturally or when threshold is reached.
*/
skipStarManualSkipCountsByKey: Record<string, number>;
/** Increment skip count for current server + track; clears stored count when threshold reached. */
recordSkipStarManualAdvance: (trackId: string) => { crossedThreshold: boolean } | null;
/** Drop persisted skip count for this track on the active server (e.g. natural playback end). */
clearSkipStarManualCountForTrack: (trackId: string) => void;
/** Random mixes, random albums, home hero: drop nonzero ratings at or below peraxis thresholds (0 = unrated, kept). */
mixMinRatingFilterEnabled: boolean;
/** 0 = ignore; 13 = cutoff (UI); exclude track rating r when 0 < r ≤ cutoff. */
mixMinRatingSong: number;
/** 0 = ignore; album entity rating from payload or `getAlbum` when missing. */
mixMinRatingAlbum: number;
/** 0 = ignore; artist rating from payload / nested OpenSubsonic fields or `getArtist`. */
mixMinRatingArtist: number;
/** Random Mix target list size (50, 75, 100, 125, or 150). */
randomMixSize: number;
/** Show "Lucky Mix" as a regular sidebar/menu item. */
showLuckyMixMenu: boolean;
/** Subsonic music folders for the active server (not persisted; refetched on login / server change). */
musicFolders: Array<{ id: string; name: string }>;
/**
* Per server: `all` = no musicFolderId param; otherwise a single folder id.
* Only one library or all no multi-folder merge.
*/
musicLibraryFilterByServer: Record<string, 'all' | string>;
/** Bumps when `setMusicLibraryFilter` runs so pages refetch catalog data. */
musicLibraryFilterVersion: number;
/**
* Per server: whether `setRating` is assumed to work for album/artist ids (OpenSubsonic-style).
* Absent key = not probed yet (`unknown` in UI).
*/
entityRatingSupportByServer: Record<string, EntityRatingSupportLevel>;
setEntityRatingSupport: (serverId: string, level: EntityRatingSupportLevel) => void;
/**
* Per server: Navidrome has the AudioMuse-AI plugin use `getSimilarSongs` (Instant Mix) and
* `getArtistInfo2` similar artists instead of Last.fm for discovery on this server.
*/
audiomuseNavidromeByServer: Record<string, boolean>;
setAudiomuseNavidromeEnabled: (serverId: string, enabled: boolean) => void;
/** From `ping` — used to show the AudioMuse toggle only on Navidrome ≥ 0.60. */
subsonicServerIdentityByServer: Record<string, SubsonicServerIdentity>;
setSubsonicServerIdentity: (serverId: string, identity: SubsonicServerIdentity) => void;
/** Instant Mix / similar path failed while this server had AudioMuse enabled (cleared on success or toggle off). */
audiomuseNavidromeIssueByServer: Record<string, boolean>;
setAudiomuseNavidromeIssue: (serverId: string, hasIssue: boolean) => void;
/**
* `getSimilarSongs` probe per server (after ping). `empty` hides the AudioMuse row; re-run by testing connection.
*/
instantMixProbeByServer: Record<string, InstantMixProbeResult>;
setInstantMixProbe: (serverId: string, result: InstantMixProbeResult) => void;
// Status
isLoggedIn: boolean;
isConnecting: boolean;
connectionError: string | null;
lastfmSessionError: boolean;
// Actions
addServer: (profile: Omit<ServerProfile, 'id'>) => string;
updateServer: (id: string, data: Partial<Omit<ServerProfile, 'id'>>) => void;
removeServer: (id: string) => void;
setServers: (servers: ServerProfile[]) => void;
setActiveServer: (id: string) => void;
setLoggedIn: (v: boolean) => void;
setConnecting: (v: boolean) => void;
setConnectionError: (e: string | null) => void;
setLastfm: (apiKey: string, apiSecret: string, sessionKey: string, username: string) => void;
connectLastfm: (sessionKey: string, username: string) => void;
disconnectLastfm: () => void;
setLastfmSessionError: (v: boolean) => void;
setScrobblingEnabled: (v: boolean) => void;
setMaxCacheMb: (v: number) => void;
setDownloadFolder: (v: string) => void;
setOfflineDownloadDir: (v: string) => void;
setExcludeAudiobooks: (v: boolean) => void;
setCustomGenreBlacklist: (v: string[]) => void;
setReplayGainEnabled: (v: boolean) => void;
setNormalizationEngine: (v: NormalizationEngine) => void;
setLoudnessTargetLufs: (v: LoudnessLufsPreset) => void;
setLoudnessPreAnalysisAttenuationDb: (v: number) => void;
resetLoudnessPreAnalysisAttenuationDbDefault: () => void;
setReplayGainMode: (v: 'track' | 'album' | 'auto') => void;
setReplayGainPreGainDb: (v: number) => void;
setReplayGainFallbackDb: (v: number) => void;
setCrossfadeEnabled: (v: boolean) => void;
setCrossfadeSecs: (v: number) => void;
setGaplessEnabled: (v: boolean) => void;
setTrackPreviewsEnabled: (v: boolean) => void;
setTrackPreviewLocation: (location: TrackPreviewLocation, enabled: boolean) => void;
setTrackPreviewStartRatio: (v: number) => void;
setTrackPreviewDurationSec: (v: number) => void;
setPreloadMode: (v: 'off' | 'balanced' | 'early' | 'custom') => void;
setPreloadCustomSeconds: (v: number) => void;
setInfiniteQueueEnabled: (v: boolean) => void;
setPreservePlayNextOrder: (v: boolean) => void;
setShowArtistImages: (v: boolean) => void;
setShowTrayIcon: (v: boolean) => void;
setMinimizeToTray: (v: boolean) => void;
setShowOrbitTrigger: (v: boolean) => void;
setDiscordRichPresence: (v: boolean) => void;
setDiscordCoverSource: (v: DiscordCoverSource) => void;
setEnableBandsintown: (v: boolean) => void;
setDiscordTemplateDetails: (v: string) => void;
setDiscordTemplateState: (v: string) => void;
setDiscordTemplateLargeText: (v: string) => void;
setUseCustomTitlebar: (v: boolean) => void;
setPreloadMiniPlayer: (v: boolean) => void;
setLinuxWebkitKineticScroll: (v: boolean) => void;
setLoggingMode: (v: LoggingMode) => void;
setNowPlayingEnabled: (v: boolean) => void;
setLyricsServerFirst: (v: boolean) => void;
setEnableNeteaselyrics: (v: boolean) => void;
setLyricsSources: (sources: LyricsSourceConfig[]) => void;
setLyricsMode: (v: 'standard' | 'lyricsplus') => void;
setLyricsStaticOnly: (v: boolean) => void;
setShowFullscreenLyrics: (v: boolean) => void;
setFsLyricsStyle: (v: 'rail' | 'apple') => void;
setSidebarLyricsStyle: (v: 'classic' | 'apple') => void;
setShowFsArtistPortrait: (v: boolean) => void;
setFsPortraitDim: (v: number) => void;
setShowChangelogOnUpdate: (v: boolean) => void;
setLastSeenChangelogVersion: (v: string) => void;
setSeekbarStyle: (v: SeekbarStyle) => void;
setQueueNowPlayingCollapsed: (v: boolean) => void;
setEnableHiRes: (v: boolean) => void;
setAudioOutputDevice: (v: string | null) => void;
setHotCacheEnabled: (v: boolean) => void;
setHotCacheMaxMb: (v: number) => void;
setHotCacheDebounceSec: (v: number) => void;
setHotCacheDownloadDir: (v: string) => void;
setSkipStarOnManualSkipsEnabled: (v: boolean) => void;
setSkipStarManualSkipThreshold: (v: number) => void;
setMixMinRatingFilterEnabled: (v: boolean) => void;
setMixMinRatingSong: (v: number) => void;
setMixMinRatingAlbum: (v: number) => void;
setMixMinRatingArtist: (v: number) => void;
setRandomMixSize: (v: number) => void;
setShowLuckyMixMenu: (v: boolean) => void;
setMusicFolders: (folders: Array<{ id: string; name: string }>) => void;
setMusicLibraryFilter: (folderId: 'all' | string) => void;
/** Navigation style for Mix pages: single hub ('hub') or separate sidebar entries ('separate'). */
randomNavMode: 'hub' | 'separate';
setRandomNavMode: (v: 'hub' | 'separate') => void;
logout: () => void;
// Derived
getBaseUrl: () => string;
getActiveServer: () => ServerProfile | undefined;
}
function generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).slice(2);
}
/** Upper bound for mix min-rating thresholds (UI shows five stars, only 1…this many are selectable). */
export const MIX_MIN_RATING_FILTER_MAX_STARS = 3;
function clampMixFilterMinStars(v: number): number {
if (!Number.isFinite(v)) return 0;
return Math.max(0, Math.min(MIX_MIN_RATING_FILTER_MAX_STARS, Math.round(v)));
}
export const RANDOM_MIX_SIZE_OPTIONS: readonly number[] = [50, 75, 100, 125, 150];
function clampRandomMixSize(v: number): number {
if (!Number.isFinite(v)) return 50;
// Snap to the nearest allowed option so a tampered persisted value can't break the picker.
let nearest = RANDOM_MIX_SIZE_OPTIONS[0];
let bestDelta = Math.abs(v - nearest);
for (const opt of RANDOM_MIX_SIZE_OPTIONS) {
const d = Math.abs(v - opt);
if (d < bestDelta) { nearest = opt; bestDelta = d; }
}
return nearest;
}
function clampSkipStarThreshold(v: number): number {
if (!Number.isFinite(v)) return 3;
return Math.max(1, Math.min(99, Math.round(v)));
}
function skipStarCountStorageKey(serverId: string | null | undefined, trackId: string): string {
return `${serverId ?? ''}\u001f${trackId}`;
}
function sanitizeSkipStarCounts(raw: unknown): Record<string, number> {
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
const next: Record<string, number> = {};
for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
const n = Number(v);
if (Number.isFinite(n) && n > 0) next[k] = Math.min(Math.floor(n), 1_000_000);
}
return next;
}
export const useAuthStore = create<AuthState>()( export const useAuthStore = create<AuthState>()(
persist( persist(
+40
View File
@@ -0,0 +1,40 @@
import type {
LoudnessLufsPreset,
LyricsSourceConfig,
TrackPreviewLocation,
TrackPreviewLocations,
} from './authStoreTypes';
export const LOUDNESS_LUFS_PRESETS: LoudnessLufsPreset[] = [-16, -14, -12, -10];
/** Settings default + Rust engine cold default until `audio_set_normalization` runs. */
export const DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB = -4.5;
export const TRACK_PREVIEW_LOCATIONS: readonly TrackPreviewLocation[] = [
'suggestions',
'albums',
'playlists',
'favorites',
'artist',
'randomMix',
];
export const DEFAULT_TRACK_PREVIEW_LOCATIONS: TrackPreviewLocations = {
suggestions: true,
albums: true,
playlists: true,
favorites: true,
artist: true,
randomMix: true,
};
export const DEFAULT_LYRICS_SOURCES: LyricsSourceConfig[] = [
{ id: 'server', enabled: true },
{ id: 'lrclib', enabled: true },
{ id: 'netease', enabled: false },
];
/** Upper bound for mix min-rating thresholds (UI shows five stars, only 1…this many are selectable). */
export const MIX_MIN_RATING_FILTER_MAX_STARS = 3;
export const RANDOM_MIX_SIZE_OPTIONS: readonly number[] = [50, 75, 100, 125, 150];
+61
View File
@@ -0,0 +1,61 @@
import { clampStoredLoudnessPreAnalysisAttenuationRefDb } from '../utils/loudnessPreAnalysisSlider';
import {
DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB,
LOUDNESS_LUFS_PRESETS,
MIX_MIN_RATING_FILTER_MAX_STARS,
RANDOM_MIX_SIZE_OPTIONS,
} from './authStoreDefaults';
import type { LoudnessLufsPreset } from './authStoreTypes';
export function generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).slice(2);
}
export function sanitizeLoudnessLufsPreset(v: unknown, fallback: LoudnessLufsPreset): LoudnessLufsPreset {
return (LOUDNESS_LUFS_PRESETS as readonly number[]).includes(v as number)
? (v as LoudnessLufsPreset)
: fallback;
}
export function sanitizeLoudnessPreAnalysisFromStorage(v: unknown): number {
const n = typeof v === 'number' ? v : Number(v);
if (!Number.isFinite(n)) return DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB;
return clampStoredLoudnessPreAnalysisAttenuationRefDb(n);
}
export function clampMixFilterMinStars(v: unknown): number {
const n = typeof v === 'number' ? v : Number(v);
if (!Number.isFinite(n)) return 0;
return Math.max(0, Math.min(MIX_MIN_RATING_FILTER_MAX_STARS, Math.round(n)));
}
export function clampRandomMixSize(v: number): number {
if (!Number.isFinite(v)) return 50;
// Snap to the nearest allowed option so a tampered persisted value can't break the picker.
let nearest = RANDOM_MIX_SIZE_OPTIONS[0];
let bestDelta = Math.abs(v - nearest);
for (const opt of RANDOM_MIX_SIZE_OPTIONS) {
const d = Math.abs(v - opt);
if (d < bestDelta) { nearest = opt; bestDelta = d; }
}
return nearest;
}
export function clampSkipStarThreshold(v: number): number {
if (!Number.isFinite(v)) return 3;
return Math.max(1, Math.min(99, Math.round(v)));
}
export function skipStarCountStorageKey(serverId: string | null | undefined, trackId: string): string {
return `${serverId ?? ''}\u001f${trackId}`;
}
export function sanitizeSkipStarCounts(raw: unknown): Record<string, number> {
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
const next: Record<string, number> = {};
for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
const n = Number(v);
if (Number.isFinite(n) && n > 0) next[k] = Math.min(Math.floor(n), 1_000_000);
}
return next;
}
+309
View File
@@ -0,0 +1,309 @@
import type { EntityRatingSupportLevel } from '../api/subsonic';
import type {
InstantMixProbeResult,
SubsonicServerIdentity,
} from '../utils/subsonicServerIdentity';
export interface ServerProfile {
id: string;
name: string;
url: string;
username: string;
password: string;
}
export type SeekbarStyle = 'truewave' | 'pseudowave' | 'linedot' | 'bar' | 'thick' | 'segmented' | 'neon' | 'pulsewave' | 'particletrail' | 'liquidfill' | 'retrotape';
export type LoggingMode = 'off' | 'normal' | 'debug';
export type NormalizationEngine = 'off' | 'replaygain' | 'loudness';
export type DiscordCoverSource = 'none' | 'apple' | 'server';
/** Integrated-loudness target presets (Settings + analysis). */
export type LoudnessLufsPreset = -16 | -14 | -12 | -10;
export type LyricsSourceId = 'server' | 'lrclib' | 'netease';
export interface LyricsSourceConfig { id: LyricsSourceId; enabled: boolean; }
export type TrackPreviewLocation =
| 'suggestions'
| 'albums'
| 'playlists'
| 'favorites'
| 'artist'
| 'randomMix';
export type TrackPreviewLocations = Record<TrackPreviewLocation, boolean>;
export interface AuthState {
// Multi-server
servers: ServerProfile[];
activeServerId: string | null;
// Last.fm (global)
lastfmApiKey: string;
lastfmApiSecret: string;
lastfmSessionKey: string;
lastfmUsername: string;
// Settings (global)
scrobblingEnabled: boolean;
maxCacheMb: number;
downloadFolder: string;
offlineDownloadDir: string;
excludeAudiobooks: boolean;
customGenreBlacklist: string[];
replayGainEnabled: boolean;
normalizationEngine: NormalizationEngine;
loudnessTargetLufs: LoudnessLufsPreset;
/**
* dB extra quieting until loudness is saved, **calibrated for 14 LUFS** target; engine applies
* `+ (loudnessTargetLufs - (14))` for other targets. See `effectiveLoudnessPreAnalysisAttenuationDb`.
*/
loudnessPreAnalysisAttenuationDb: number;
/** Persisted: stored pre is ref @ 14 (v1+); legacy falsey entries migrate once in onRehydrate. */
loudnessPreIsRefV1?: boolean;
replayGainMode: 'track' | 'album' | 'auto';
replayGainPreGainDb: number; // added to RG gain for tagged files (0…+6 dB)
replayGainFallbackDb: number; // gain for untagged files / radio (-6…0 dB)
crossfadeEnabled: boolean;
crossfadeSecs: number;
gaplessEnabled: boolean;
/** Show inline Play+Preview buttons in tracklists. Default on per Q3. Master kill switch — when off, all locations are off. */
trackPreviewsEnabled: boolean;
/** Per-location toggles. Only honoured when `trackPreviewsEnabled` is true. */
trackPreviewLocations: TrackPreviewLocations;
/** Mid-track start position as a 0…1 ratio. Default 0.33 = 33%. */
trackPreviewStartRatio: number;
/** Preview window length in seconds. Default 30 s. */
trackPreviewDurationSec: number;
preloadMode: 'off' | 'balanced' | 'early' | 'custom';
preloadCustomSeconds: number;
infiniteQueueEnabled: boolean;
preservePlayNextOrder: boolean;
showArtistImages: boolean;
showTrayIcon: boolean;
minimizeToTray: boolean;
/** Whether the "Orbit" topbar trigger is rendered. Users who never
* touch Orbit can hide it so the header stays uncluttered. */
showOrbitTrigger: boolean;
discordRichPresence: boolean;
discordCoverSource: DiscordCoverSource;
/** Opt-in: fetch upcoming tour dates from Bandsintown for the Now-Playing info panel. */
enableBandsintown: boolean;
discordTemplateDetails: string;
discordTemplateState: string;
discordTemplateLargeText: string;
useCustomTitlebar: boolean;
/** Pre-build the mini-player webview at app start on Linux/macOS so content is available instantly
* on first open. Ignored on Windows that platform always pre-creates as a hang workaround. */
preloadMiniPlayer: boolean;
/** Linux WebKitGTK: smooth wheel on when true; off only after explicit opt-out in Settings. */
linuxWebkitKineticScroll: boolean;
/** Runtime backend logging level. */
loggingMode: LoggingMode;
nowPlayingEnabled: boolean;
lyricsServerFirst: boolean;
enableNeteaselyrics: boolean;
lyricsSources: LyricsSourceConfig[];
/**
* `'standard'` server + lrclib + netease pipeline (configurable order).
* `'lyricsplus'` YouLyPlus / lyricsplus first, silent fallback to standard
* pipeline when no data is returned.
*/
lyricsMode: 'standard' | 'lyricsplus';
/**
* Render synced lines as static text (no auto-scroll, no word highlighting).
* Honoured in both lyrics modes.
*/
lyricsStaticOnly: boolean;
showFullscreenLyrics: boolean;
/** 'rail' = classic 5-line sliding rail; 'apple' = full-screen scrolling list */
fsLyricsStyle: 'rail' | 'apple';
/** Sidebar lyrics scroll style: 'classic' = scrollIntoView center; 'apple' = scroll to 35% */
sidebarLyricsStyle: 'classic' | 'apple';
showFsArtistPortrait: boolean;
/** Portrait dimming 0100 (percent), applied as CSS rgba alpha */
fsPortraitDim: number;
showChangelogOnUpdate: boolean;
lastSeenChangelogVersion: string;
seekbarStyle: SeekbarStyle;
/** Persisted UI toggle: is the Now Playing section in queue panel collapsed */
queueNowPlayingCollapsed: boolean;
/** Alpha: native hi-res sample rate output (disabled = safe 44.1 kHz mode) */
enableHiRes: boolean;
/** Selected audio output device name. null = system default. */
audioOutputDevice: string | null;
/** Alpha: ephemeral queue prefetch cache on disk */
hotCacheEnabled: boolean;
hotCacheMaxMb: number;
hotCacheDebounceSec: number;
/** Parent directory; actual cache is `<dir>/psysonic-hot-cache/`. Empty = app data. */
hotCacheDownloadDir: string;
/** After this many manual skips of the same track, set track rating to 1 if still unrated (below 1 star). */
skipStarOnManualSkipsEnabled: boolean;
/** Manual skips per track before applying rating 1 (when enabled). */
skipStarManualSkipThreshold: number;
/**
* Manual Next-count per track for skip1. Key = `${serverId}\u001f${trackId}`
* (empty serverId when none). Persisted; cleared when the track finishes naturally or when threshold is reached.
*/
skipStarManualSkipCountsByKey: Record<string, number>;
/** Increment skip count for current server + track; clears stored count when threshold reached. */
recordSkipStarManualAdvance: (trackId: string) => { crossedThreshold: boolean } | null;
/** Drop persisted skip count for this track on the active server (e.g. natural playback end). */
clearSkipStarManualCountForTrack: (trackId: string) => void;
/** Random mixes, random albums, home hero: drop nonzero ratings at or below peraxis thresholds (0 = unrated, kept). */
mixMinRatingFilterEnabled: boolean;
/** 0 = ignore; 13 = cutoff (UI); exclude track rating r when 0 < r ≤ cutoff. */
mixMinRatingSong: number;
/** 0 = ignore; album entity rating from payload or `getAlbum` when missing. */
mixMinRatingAlbum: number;
/** 0 = ignore; artist rating from payload / nested OpenSubsonic fields or `getArtist`. */
mixMinRatingArtist: number;
/** Random Mix target list size (50, 75, 100, 125, or 150). */
randomMixSize: number;
/** Show "Lucky Mix" as a regular sidebar/menu item. */
showLuckyMixMenu: boolean;
/** Subsonic music folders for the active server (not persisted; refetched on login / server change). */
musicFolders: Array<{ id: string; name: string }>;
/**
* Per server: `all` = no musicFolderId param; otherwise a single folder id.
* Only one library or all no multi-folder merge.
*/
musicLibraryFilterByServer: Record<string, 'all' | string>;
/** Bumps when `setMusicLibraryFilter` runs so pages refetch catalog data. */
musicLibraryFilterVersion: number;
/**
* Per server: whether `setRating` is assumed to work for album/artist ids (OpenSubsonic-style).
* Absent key = not probed yet (`unknown` in UI).
*/
entityRatingSupportByServer: Record<string, EntityRatingSupportLevel>;
setEntityRatingSupport: (serverId: string, level: EntityRatingSupportLevel) => void;
/**
* Per server: Navidrome has the AudioMuse-AI plugin use `getSimilarSongs` (Instant Mix) and
* `getArtistInfo2` similar artists instead of Last.fm for discovery on this server.
*/
audiomuseNavidromeByServer: Record<string, boolean>;
setAudiomuseNavidromeEnabled: (serverId: string, enabled: boolean) => void;
/** From `ping` — used to show the AudioMuse toggle only on Navidrome ≥ 0.60. */
subsonicServerIdentityByServer: Record<string, SubsonicServerIdentity>;
setSubsonicServerIdentity: (serverId: string, identity: SubsonicServerIdentity) => void;
/** Instant Mix / similar path failed while this server had AudioMuse enabled (cleared on success or toggle off). */
audiomuseNavidromeIssueByServer: Record<string, boolean>;
setAudiomuseNavidromeIssue: (serverId: string, hasIssue: boolean) => void;
/**
* `getSimilarSongs` probe per server (after ping). `empty` hides the AudioMuse row; re-run by testing connection.
*/
instantMixProbeByServer: Record<string, InstantMixProbeResult>;
setInstantMixProbe: (serverId: string, result: InstantMixProbeResult) => void;
// Status
isLoggedIn: boolean;
isConnecting: boolean;
connectionError: string | null;
lastfmSessionError: boolean;
// Actions
addServer: (profile: Omit<ServerProfile, 'id'>) => string;
updateServer: (id: string, data: Partial<Omit<ServerProfile, 'id'>>) => void;
removeServer: (id: string) => void;
setServers: (servers: ServerProfile[]) => void;
setActiveServer: (id: string) => void;
setLoggedIn: (v: boolean) => void;
setConnecting: (v: boolean) => void;
setConnectionError: (e: string | null) => void;
setLastfm: (apiKey: string, apiSecret: string, sessionKey: string, username: string) => void;
connectLastfm: (sessionKey: string, username: string) => void;
disconnectLastfm: () => void;
setLastfmSessionError: (v: boolean) => void;
setScrobblingEnabled: (v: boolean) => void;
setMaxCacheMb: (v: number) => void;
setDownloadFolder: (v: string) => void;
setOfflineDownloadDir: (v: string) => void;
setExcludeAudiobooks: (v: boolean) => void;
setCustomGenreBlacklist: (v: string[]) => void;
setReplayGainEnabled: (v: boolean) => void;
setNormalizationEngine: (v: NormalizationEngine) => void;
setLoudnessTargetLufs: (v: LoudnessLufsPreset) => void;
setLoudnessPreAnalysisAttenuationDb: (v: number) => void;
resetLoudnessPreAnalysisAttenuationDbDefault: () => void;
setReplayGainMode: (v: 'track' | 'album' | 'auto') => void;
setReplayGainPreGainDb: (v: number) => void;
setReplayGainFallbackDb: (v: number) => void;
setCrossfadeEnabled: (v: boolean) => void;
setCrossfadeSecs: (v: number) => void;
setGaplessEnabled: (v: boolean) => void;
setTrackPreviewsEnabled: (v: boolean) => void;
setTrackPreviewLocation: (location: TrackPreviewLocation, enabled: boolean) => void;
setTrackPreviewStartRatio: (v: number) => void;
setTrackPreviewDurationSec: (v: number) => void;
setPreloadMode: (v: 'off' | 'balanced' | 'early' | 'custom') => void;
setPreloadCustomSeconds: (v: number) => void;
setInfiniteQueueEnabled: (v: boolean) => void;
setPreservePlayNextOrder: (v: boolean) => void;
setShowArtistImages: (v: boolean) => void;
setShowTrayIcon: (v: boolean) => void;
setMinimizeToTray: (v: boolean) => void;
setShowOrbitTrigger: (v: boolean) => void;
setDiscordRichPresence: (v: boolean) => void;
setDiscordCoverSource: (v: DiscordCoverSource) => void;
setEnableBandsintown: (v: boolean) => void;
setDiscordTemplateDetails: (v: string) => void;
setDiscordTemplateState: (v: string) => void;
setDiscordTemplateLargeText: (v: string) => void;
setUseCustomTitlebar: (v: boolean) => void;
setPreloadMiniPlayer: (v: boolean) => void;
setLinuxWebkitKineticScroll: (v: boolean) => void;
setLoggingMode: (v: LoggingMode) => void;
setNowPlayingEnabled: (v: boolean) => void;
setLyricsServerFirst: (v: boolean) => void;
setEnableNeteaselyrics: (v: boolean) => void;
setLyricsSources: (sources: LyricsSourceConfig[]) => void;
setLyricsMode: (v: 'standard' | 'lyricsplus') => void;
setLyricsStaticOnly: (v: boolean) => void;
setShowFullscreenLyrics: (v: boolean) => void;
setFsLyricsStyle: (v: 'rail' | 'apple') => void;
setSidebarLyricsStyle: (v: 'classic' | 'apple') => void;
setShowFsArtistPortrait: (v: boolean) => void;
setFsPortraitDim: (v: number) => void;
setShowChangelogOnUpdate: (v: boolean) => void;
setLastSeenChangelogVersion: (v: string) => void;
setSeekbarStyle: (v: SeekbarStyle) => void;
setQueueNowPlayingCollapsed: (v: boolean) => void;
setEnableHiRes: (v: boolean) => void;
setAudioOutputDevice: (v: string | null) => void;
setHotCacheEnabled: (v: boolean) => void;
setHotCacheMaxMb: (v: number) => void;
setHotCacheDebounceSec: (v: number) => void;
setHotCacheDownloadDir: (v: string) => void;
setSkipStarOnManualSkipsEnabled: (v: boolean) => void;
setSkipStarManualSkipThreshold: (v: number) => void;
setMixMinRatingFilterEnabled: (v: boolean) => void;
setMixMinRatingSong: (v: number) => void;
setMixMinRatingAlbum: (v: number) => void;
setMixMinRatingArtist: (v: number) => void;
setRandomMixSize: (v: number) => void;
setShowLuckyMixMenu: (v: boolean) => void;
setMusicFolders: (folders: Array<{ id: string; name: string }>) => void;
setMusicLibraryFilter: (folderId: 'all' | string) => void;
/** Navigation style for Mix pages: single hub ('hub') or separate sidebar entries ('separate'). */
randomNavMode: 'hub' | 'separate';
setRandomNavMode: (v: 'hub' | 'separate') => void;
logout: () => void;
// Derived
getBaseUrl: () => string;
getActiveServer: () => ServerProfile | undefined;
}
+1 -2
View File
@@ -1,6 +1,5 @@
import type { ServerProfile } from './authStoreTypes';
import { create } from 'zustand'; import { create } from 'zustand';
import type { ServerProfile } from './authStore';
let _resolve: ((server: ServerProfile | null) => void) | null = null; let _resolve: ((server: ServerProfile | null) => void) | null = null;
interface OrbitAccountPickerStore { interface OrbitAccountPickerStore {
+2 -1
View File
@@ -1,8 +1,9 @@
import type { TrackPreviewLocation } from './authStoreTypes';
import { create } from 'zustand'; import { create } from 'zustand';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { buildStreamUrl } from '../api/subsonic'; import { buildStreamUrl } from '../api/subsonic';
import { usePlayerStore } from './playerStore'; import { usePlayerStore } from './playerStore';
import { useAuthStore, type TrackPreviewLocation } from './authStore'; import { useAuthStore } from './authStore';
import { useOrbitStore } from './orbitStore'; import { useOrbitStore } from './orbitStore';
/** Minimal track info needed to surface the preview in the player bar UI. */ /** Minimal track info needed to surface the preview in the player bar UI. */
+1 -1
View File
@@ -5,8 +5,8 @@
* the fields the test cares about. Keeps tests focused on behaviour rather * the fields the test cares about. Keeps tests focused on behaviour rather
* than on assembling boilerplate. * than on assembling boilerplate.
*/ */
import type { ServerProfile } from '@/store/authStoreTypes';
import type { Track } from '@/store/playerStoreTypes'; import type { Track } from '@/store/playerStoreTypes';
import type { ServerProfile } from '@/store/authStore';
import type { SubsonicSong } from '@/api/subsonic'; import type { SubsonicSong } from '@/api/subsonic';
let trackCounter = 0; let trackCounter = 0;
+1 -1
View File
@@ -1,5 +1,5 @@
import type { ServerProfile } from '../store/authStoreTypes';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import type { ServerProfile } from '../store/authStore';
import { serverListDisplayLabel, shortHostFromServerUrl } from './serverDisplayName'; import { serverListDisplayLabel, shortHostFromServerUrl } from './serverDisplayName';
function srv(p: Partial<ServerProfile> & Pick<ServerProfile, 'id'>): ServerProfile { function srv(p: Partial<ServerProfile> & Pick<ServerProfile, 'id'>): ServerProfile {
+1 -2
View File
@@ -1,5 +1,4 @@
import type { ServerProfile } from '../store/authStore'; import type { ServerProfile } from '../store/authStoreTypes';
/** Host (+ port) from a server base URL, e.g. `https://music.one.com/foo` → `music.one.com`. */ /** Host (+ port) from a server base URL, e.g. `https://music.one.com/foo` → `music.one.com`. */
export function shortHostFromServerUrl(urlRaw: string): string { export function shortHostFromServerUrl(urlRaw: string): string {
const t = urlRaw.trim(); const t = urlRaw.trim();
+1 -2
View File
@@ -1,5 +1,4 @@
import type { ServerProfile } from '../store/authStore'; import type { ServerProfile } from '../store/authStoreTypes';
/** Library share (track / album / artist / queue). Same naming family as `psysonic1-` server invites. */ /** Library share (track / album / artist / queue). Same naming family as `psysonic1-` server invites. */
export const PSYSONIC_SHARE_PREFIX = 'psysonic2-'; export const PSYSONIC_SHARE_PREFIX = 'psysonic2-';
+1 -1
View File
@@ -1,5 +1,5 @@
import type { ServerProfile } from '../store/authStoreTypes';
import { pingWithCredentials, scheduleInstantMixProbeForServer } from '../api/subsonic'; import { pingWithCredentials, scheduleInstantMixProbeForServer } from '../api/subsonic';
import type { ServerProfile } from '../store/authStore';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { useOrbitStore } from '../store/orbitStore'; import { useOrbitStore } from '../store/orbitStore';
import { endOrbitSession, leaveOrbitSession } from './orbit'; import { endOrbitSession, leaveOrbitSession } from './orbit';