mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
refactor(utils): group utils/ files into topic folders (Phase L, part 1) (#689)
111 of 122 top-level src/utils/ files move into 16 topic folders (audio, cache, cover, share, server, playback, playlist, deviceSync, waveform, mix, format, export, changelog, ui, perf, componentHelpers). True singletons with no cluster stay at the utils/ root. Pure file-move: a path-aware codemod rewrote 539 relative-import specifiers across 275 files; no logic touched. The hot-path coverage gate list (.github/frontend-hot-path-files.txt) is updated to the new paths for the 11 gated utils files — a mechanical consequence of the move, not a CI change. tsc is green.
This commit is contained in:
committed by
GitHub
parent
2409a1fec8
commit
7a7a9f5e6b
@@ -1,4 +1,4 @@
|
||||
import { EQ_BANDS } from '../store/eqStore';
|
||||
import { EQ_BANDS } from '../../store/eqStore';
|
||||
|
||||
// ─── Frequency response canvas ────────────────────────────────────────────────
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Originally lived in `playerStore.ts`; extracted in M0 of the frontend
|
||||
* refactor (2026-05-12).
|
||||
*/
|
||||
import type { Track } from '../store/playerStoreTypes';
|
||||
import type { Track } from '../../store/playerStoreTypes';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { resolveReplayGainDb } from './resolveReplayGainDb';
|
||||
describe('resolveReplayGainDb', () => {
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Track } from '../store/playerStoreTypes';
|
||||
import type { Track } from '../../store/playerStoreTypes';
|
||||
/**
|
||||
* Resolve the ReplayGain dB value for a track based on the configured mode.
|
||||
* In 'auto' mode, picks album-gain when an adjacent queue neighbour shares the
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
* - notFound entries: 7 days. Lets the user / server admin add lyrics
|
||||
* later without an indefinite negative cache.
|
||||
*/
|
||||
import type { CachedLyrics } from '../hooks/useLyrics';
|
||||
import type { CachedLyrics } from '../../hooks/useLyrics';
|
||||
|
||||
const DB_NAME = 'psysonic-lyrics-cache';
|
||||
const STORE_NAME = 'lyrics';
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { ColDef } from './useTracklistColumns';
|
||||
import type { ColDef } from '../useTracklistColumns';
|
||||
|
||||
export function formatDuration(seconds: number): string {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../constants/appScroll';
|
||||
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../../constants/appScroll';
|
||||
|
||||
export const SIDEBAR_COLLAPSED_STORAGE_KEY = 'psysonic_sidebar_collapsed';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IS_LINUX, IS_MACOS, IS_WINDOWS } from './platform';
|
||||
import { IS_LINUX, IS_MACOS, IS_WINDOWS } from '../platform';
|
||||
|
||||
export const SKIP_KEY = 'psysonic_skipped_update_version';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SubsonicArtist } from '../api/subsonicTypes';
|
||||
import type { SubsonicArtist } from '../../api/subsonicTypes';
|
||||
|
||||
export const ALL_SENTINEL = 'ALL';
|
||||
export const ALPHABET = [ALL_SENTINEL, '#', ...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')];
|
||||
+11
-11
@@ -1,17 +1,17 @@
|
||||
import { join } from '@tauri-apps/api/path';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { getSimilarSongs2, getSimilarSongs, getTopSongs } from '../api/subsonicArtists';
|
||||
import { buildDownloadUrl } from '../api/subsonicStreamUrl';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import type { Track } from '../store/playerStoreTypes';
|
||||
import { useZipDownloadStore } from '../store/zipDownloadStore';
|
||||
import { useDownloadModalStore } from '../store/downloadModalStore';
|
||||
import type { EntityShareKind } from './shareLink';
|
||||
import { copyEntityShareLink } from './copyEntityShareLink';
|
||||
import { getSimilarSongs2, getSimilarSongs, getTopSongs } from '../../api/subsonicArtists';
|
||||
import { buildDownloadUrl } from '../../api/subsonicStreamUrl';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { usePlayerStore } from '../../store/playerStore';
|
||||
import type { Track } from '../../store/playerStoreTypes';
|
||||
import { useZipDownloadStore } from '../../store/zipDownloadStore';
|
||||
import { useDownloadModalStore } from '../../store/downloadModalStore';
|
||||
import type { EntityShareKind } from '../share/shareLink';
|
||||
import { copyEntityShareLink } from '../share/copyEntityShareLink';
|
||||
import { sanitizeFilename, shuffleArray } from './contextMenuHelpers';
|
||||
import { songToTrack } from './songToTrack';
|
||||
import { showToast } from './toast';
|
||||
import { songToTrack } from '../playback/songToTrack';
|
||||
import { showToast } from '../ui/toast';
|
||||
|
||||
export async function copyShareLink(
|
||||
kind: EntityShareKind,
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useConfirmModalStore } from '../store/confirmModalStore';
|
||||
import { useConfirmModalStore } from '../../store/confirmModalStore';
|
||||
|
||||
/** Psysonic smart playlists (Navidrome); not valid targets for manual add-to-playlist. */
|
||||
export const SMART_PLAYLIST_PREFIX = 'psy-smart-';
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import type React from 'react';
|
||||
import type { SubsonicAlbum, SubsonicDirectoryEntry } from '../api/subsonicTypes';
|
||||
import type { Track } from '../store/playerStoreTypes';
|
||||
import type { SubsonicAlbum, SubsonicDirectoryEntry } from '../../api/subsonicTypes';
|
||||
import type { Track } from '../../store/playerStoreTypes';
|
||||
|
||||
export type ColumnKind = 'roots' | 'indexes' | 'directory';
|
||||
export type NavPos = { colIndex: number; rowIndex: number };
|
||||
@@ -1,5 +1,5 @@
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import type { MiniSyncPayload, MiniTrackInfo } from './miniPlayerBridge';
|
||||
import { usePlayerStore } from '../../store/playerStore';
|
||||
import type { MiniSyncPayload, MiniTrackInfo } from '../miniPlayerBridge';
|
||||
|
||||
export const COLLAPSED_SIZE = { w: 340, h: 260 };
|
||||
export const EXPANDED_SIZE = { w: 340, h: 500 };
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SubsonicSong } from '../api/subsonicTypes';
|
||||
import type { SubsonicSong } from '../../api/subsonicTypes';
|
||||
|
||||
export function formatTime(s: number): string {
|
||||
if (!s || isNaN(s)) return '0:00';
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import type { SubsonicSong } from '../api/subsonicTypes';
|
||||
import { formatHumanHoursMinutes } from './formatHumanDuration';
|
||||
import type { SubsonicSong } from '../../api/subsonicTypes';
|
||||
import { formatHumanHoursMinutes } from '../format/formatHumanDuration';
|
||||
|
||||
export function sanitizeFilename(name: string): string {
|
||||
return name
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Star } from 'lucide-react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { Track } from '../store/playerStoreTypes';
|
||||
import type { Track } from '../../store/playerStoreTypes';
|
||||
|
||||
export type DurationMode = 'total' | 'remaining' | 'eta';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SubsonicSong } from '../api/subsonicTypes';
|
||||
import { passesMixMinRatings, type MixMinRatingsConfig } from './mixRatingFilter';
|
||||
import type { SubsonicSong } from '../../api/subsonicTypes';
|
||||
import { passesMixMinRatings, type MixMinRatingsConfig } from '../mix/mixRatingFilter';
|
||||
|
||||
export const AUDIOBOOK_GENRES = [
|
||||
'hörbuch', 'hoerbuch', 'hörspiel', 'hoerspiel',
|
||||
+7
-7
@@ -1,12 +1,12 @@
|
||||
import type React from 'react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { uploadArtistImage } from '../api/subsonicPlaylists';
|
||||
import { setRating, star, unstar } from '../api/subsonicStarRating';
|
||||
import type { SubsonicArtist } from '../api/subsonicTypes';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { copyEntityShareLink } from './copyEntityShareLink';
|
||||
import { invalidateCoverArt } from './imageCache';
|
||||
import { showToast } from './toast';
|
||||
import { uploadArtistImage } from '../../api/subsonicPlaylists';
|
||||
import { setRating, star, unstar } from '../../api/subsonicStarRating';
|
||||
import type { SubsonicArtist } from '../../api/subsonicTypes';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { copyEntityShareLink } from '../share/copyEntityShareLink';
|
||||
import { invalidateCoverArt } from '../imageCache';
|
||||
import { showToast } from '../ui/toast';
|
||||
|
||||
export interface RunArtistEntityRatingDeps {
|
||||
artist: SubsonicArtist | null;
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
import type { TFunction } from 'i18next';
|
||||
import { getAlbum } from '../api/subsonicLibrary';
|
||||
import { getSimilarSongs2, getTopSongs } from '../api/subsonicArtists';
|
||||
import type { SubsonicAlbum, SubsonicArtist } from '../api/subsonicTypes';
|
||||
import type { Track } from '../store/playerStoreTypes';
|
||||
import { songToTrack } from './songToTrack';
|
||||
import { getAlbum } from '../../api/subsonicLibrary';
|
||||
import { getSimilarSongs2, getTopSongs } from '../../api/subsonicArtists';
|
||||
import type { SubsonicAlbum, SubsonicArtist } from '../../api/subsonicTypes';
|
||||
import type { Track } from '../../store/playerStoreTypes';
|
||||
import { songToTrack } from '../playback/songToTrack';
|
||||
|
||||
async function fetchAllTracks(albums: SubsonicAlbum[]): Promise<Track[]> {
|
||||
const results = await Promise.all(albums.map(a => getAlbum(a.id)));
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ALL_NAV_ITEMS } from '../config/navItems';
|
||||
import type { SidebarItemConfig } from '../store/sidebarStore';
|
||||
import { ALL_NAV_ITEMS } from '../../config/navItems';
|
||||
import type { SidebarItemConfig } from '../../store/sidebarStore';
|
||||
|
||||
export type SidebarNavSection = 'library' | 'system';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SubsonicSong } from '../api/subsonicTypes';
|
||||
import type { SubsonicSong } from '../../api/subsonicTypes';
|
||||
|
||||
export type SourceTab = 'playlists' | 'albums' | 'artists';
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { IS_WINDOWS } from './platform';
|
||||
import { IS_WINDOWS } from '../platform';
|
||||
|
||||
// Same sanitize rules the Rust side uses (`sanitize_path_component`): strip
|
||||
// Windows-illegal chars and control chars, trim leading/trailing dots + spaces.
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { open as openDialog } from '@tauri-apps/plugin-dialog';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { useDeviceSyncStore, type DeviceSyncSource } from '../store/deviceSyncStore';
|
||||
import { showToast } from './toast';
|
||||
import { useDeviceSyncStore, type DeviceSyncSource } from '../../store/deviceSyncStore';
|
||||
import { showToast } from '../ui/toast';
|
||||
|
||||
export interface RunDeviceSyncChooseFolderDeps {
|
||||
t: TFunction;
|
||||
+7
-7
@@ -1,12 +1,12 @@
|
||||
import type { TFunction } from 'i18next';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { buildDownloadUrl } from '../api/subsonicStreamUrl';
|
||||
import type { SubsonicSong } from '../api/subsonicTypes';
|
||||
import { useDeviceSyncStore, type DeviceSyncSource } from '../store/deviceSyncStore';
|
||||
import { useDeviceSyncJobStore } from '../store/deviceSyncJobStore';
|
||||
import { showToast } from './toast';
|
||||
import { buildDownloadUrl } from '../../api/subsonicStreamUrl';
|
||||
import type { SubsonicSong } from '../../api/subsonicTypes';
|
||||
import { useDeviceSyncStore, type DeviceSyncSource } from '../../store/deviceSyncStore';
|
||||
import { useDeviceSyncJobStore } from '../../store/deviceSyncJobStore';
|
||||
import { showToast } from '../ui/toast';
|
||||
import { trackToSyncInfo, uuid } from './deviceSyncHelpers';
|
||||
import { fetchTracksForSource } from './fetchTracksForSource';
|
||||
import { fetchTracksForSource } from '../playback/fetchTracksForSource';
|
||||
|
||||
export interface SyncDelta {
|
||||
addBytes: number;
|
||||
@@ -37,7 +37,7 @@ export async function runDeviceSyncSummaryPrompt(deps: RunDeviceSyncSummaryDeps)
|
||||
setPreSyncOpen(true);
|
||||
|
||||
try {
|
||||
const { getClient } = await import('../api/subsonicClient');
|
||||
const { getClient } = await import('../../api/subsonicClient');
|
||||
const { baseUrl, params } = getClient();
|
||||
const payload = await invoke<SyncDelta>('calculate_sync_payload', {
|
||||
sources,
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
import type React from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import type { DeviceSyncSource } from '../store/deviceSyncStore';
|
||||
import type { SubsonicSong } from '../api/subsonicTypes';
|
||||
import type { DeviceSyncSource } from '../../store/deviceSyncStore';
|
||||
import type { SubsonicSong } from '../../api/subsonicTypes';
|
||||
import { applyLegacyTemplate } from './deviceSyncLegacyTemplate';
|
||||
import { trackToSyncInfo } from './deviceSyncHelpers';
|
||||
import { fetchTracksForSource } from './fetchTracksForSource';
|
||||
import { IS_WINDOWS } from './platform';
|
||||
import { fetchTracksForSource } from '../playback/fetchTracksForSource';
|
||||
import { IS_WINDOWS } from '../platform';
|
||||
|
||||
export type MigrationPhase = 'closed' | 'loading' | 'preview' | 'executing' | 'done' | 'nothing';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { save, open as openDialog } from '@tauri-apps/plugin-dialog';
|
||||
import { writeFile, readTextFile } from '@tauri-apps/plugin-fs';
|
||||
import { version as appVersion } from '../../package.json';
|
||||
import { version as appVersion } from '../../../package.json';
|
||||
|
||||
const BACKUP_VERSION = 1;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
|
||||
import type { SubsonicAlbum } from '../api/subsonicTypes';
|
||||
import { buildCoverArtUrl, coverArtCacheKey } from '../../api/subsonicStreamUrl';
|
||||
import type { SubsonicAlbum } from '../../api/subsonicTypes';
|
||||
import React from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { getCachedBlob } from './imageCache';
|
||||
import PsysonicLogo from '../components/PsysonicLogo';
|
||||
import { getCachedBlob } from '../imageCache';
|
||||
import PsysonicLogo from '../../components/PsysonicLogo';
|
||||
|
||||
export type ExportFormat = 'story' | 'square' | 'twitter';
|
||||
export type ExportGridSize = 3 | 4 | 5;
|
||||
@@ -1,9 +1,9 @@
|
||||
import { buildCoverArtUrl } from '../api/subsonicStreamUrl';
|
||||
import { getAlbumList } from '../api/subsonicLibrary';
|
||||
import type { SubsonicAlbum } from '../api/subsonicTypes';
|
||||
import { buildCoverArtUrl } from '../../api/subsonicStreamUrl';
|
||||
import { getAlbumList } from '../../api/subsonicLibrary';
|
||||
import type { SubsonicAlbum } from '../../api/subsonicTypes';
|
||||
import { writeFile } from '@tauri-apps/plugin-fs';
|
||||
import { downloadDir, join } from '@tauri-apps/api/path';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
// Catppuccin Macchiato palette
|
||||
const M = {
|
||||
crust: '#181926',
|
||||
@@ -1,4 +1,4 @@
|
||||
import i18n from '../i18n';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
/** Totals / statistics: localized "N hours M minutes" (not track mm:ss). */
|
||||
export function formatHumanHoursMinutes(seconds: number): string {
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import { useWindowVisibility } from '../hooks/useWindowVisibility';
|
||||
import { usePlayerStore } from '../../store/playerStore';
|
||||
import { useWindowVisibility } from '../../hooks/useWindowVisibility';
|
||||
|
||||
/** Remaining time until wall-clock `deadlineMs` (m:ss or h:mm:ss). */
|
||||
export function formatPlaybackScheduleRemaining(deadlineMs: number | null, nowMs: number): string {
|
||||
@@ -2,7 +2,7 @@
|
||||
// `imageCache/`; this file keeps the orchestration entry points and re-exports
|
||||
// the public surface so existing call sites import from `imageCache` unchanged.
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { COVER_ART_REGISTERED_SIZES } from './coverArtRegisteredSizes';
|
||||
import { COVER_ART_REGISTERED_SIZES } from './cover/coverArtRegisteredSizes';
|
||||
import { STORE_NAME } from './imageCache/constants';
|
||||
import { blobCache, inflightBlobGets, rememberBlob } from './imageCache/blobCache';
|
||||
import { purgeUrlEntry, clearAllUrlEntries } from './imageCache/urlPool';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { COVER_ART_REGISTERED_SIZES } from '../coverArtRegisteredSizes';
|
||||
import { downscaleCoverBlob } from '../coverBlobDownscale';
|
||||
import { COVER_ART_REGISTERED_SIZES } from '../cover/coverArtRegisteredSizes';
|
||||
import { downscaleCoverBlob } from '../cover/coverBlobDownscale';
|
||||
import { blobCache, rememberBlob } from './blobCache';
|
||||
import { purgeUrlEntry } from './urlPool';
|
||||
import { mapBlobsFromIDB, putBlob } from './idbStore';
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { getSimilarSongs } from '../api/subsonicArtists';
|
||||
import { filterSongsToActiveLibrary, getRandomSongs } from '../api/subsonicLibrary';
|
||||
import type { SubsonicAlbum, SubsonicSong } from '../api/subsonicTypes';
|
||||
import type { Track } from '../store/playerStoreTypes';
|
||||
import { songToTrack } from '../utils/songToTrack';
|
||||
import { getSimilarSongs } from '../../api/subsonicArtists';
|
||||
import { filterSongsToActiveLibrary, getRandomSongs } from '../../api/subsonicLibrary';
|
||||
import type { SubsonicAlbum, SubsonicSong } from '../../api/subsonicTypes';
|
||||
import type { Track } from '../../store/playerStoreTypes';
|
||||
import { songToTrack } from '../playback/songToTrack';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import i18n from '../i18n';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import { useLuckyMixStore } from '../store/luckyMixStore';
|
||||
import { isLuckyMixAvailable } from '../hooks/useLuckyMixAvailable';
|
||||
import { showToast } from './toast';
|
||||
import i18n from '../../i18n';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { usePlayerStore } from '../../store/playerStore';
|
||||
import { useLuckyMixStore } from '../../store/luckyMixStore';
|
||||
import { isLuckyMixAvailable } from '../../hooks/useLuckyMixAvailable';
|
||||
import { showToast } from '../ui/toast';
|
||||
import {
|
||||
filterSongsForLuckyMixRatings,
|
||||
getMixMinRatingsConfigFromAuth,
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getTopSongs } from '../api/subsonicArtists';
|
||||
import { filterSongsToActiveLibrary, getAlbum, getAlbumList, getRandomSongs } from '../api/subsonicLibrary';
|
||||
import type { SubsonicAlbum, SubsonicSong } from '../api/subsonicTypes';
|
||||
import { getTopSongs } from '../../api/subsonicArtists';
|
||||
import { filterSongsToActiveLibrary, getAlbum, getAlbumList, getRandomSongs } from '../../api/subsonicLibrary';
|
||||
import type { SubsonicAlbum, SubsonicSong } from '../../api/subsonicTypes';
|
||||
import {
|
||||
filterSongsForLuckyMixRatings,
|
||||
type MixMinRatingsConfig,
|
||||
@@ -1,7 +1,7 @@
|
||||
import { parseSubsonicEntityStarRating, prefetchAlbumUserRatings, prefetchArtistUserRatings } from '../api/subsonicRatings';
|
||||
import { getRandomSongs } from '../api/subsonicLibrary';
|
||||
import type { SubsonicAlbum, SubsonicSong } from '../api/subsonicTypes';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { parseSubsonicEntityStarRating, prefetchAlbumUserRatings, prefetchArtistUserRatings } from '../../api/subsonicRatings';
|
||||
import { getRandomSongs } from '../../api/subsonicLibrary';
|
||||
import type { SubsonicAlbum, SubsonicSong } from '../../api/subsonicTypes';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
/** Default target list size for Random Mix; per-call override via `fetchRandomMixSongsUntilFull(c, { targetSize })`. */
|
||||
export const RANDOM_MIX_TARGET_SIZE = 50;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createPlaylist, deletePlaylist, getPlaylist, getPlaylists, updatePlaylist } from '../../api/subsonicPlaylists';
|
||||
import { getSong } from '../../api/subsonicLibrary';
|
||||
import { songToTrack } from '../songToTrack';
|
||||
import { songToTrack } from '../playback/songToTrack';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { useOrbitStore } from '../../store/orbitStore';
|
||||
import { usePlayerStore } from '../../store/playerStore';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createPlaylist, deletePlaylist } from '../../api/subsonicPlaylists';
|
||||
import { getSong } from '../../api/subsonicLibrary';
|
||||
import { songToTrack } from '../songToTrack';
|
||||
import { songToTrack } from '../playback/songToTrack';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { useOrbitStore } from '../../store/orbitStore';
|
||||
import { usePlayerStore } from '../../store/playerStore';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { decodeOrbitSharePayloadFromText, encodeSharePayload } from '../shareLink';
|
||||
import { decodeOrbitSharePayloadFromText, encodeSharePayload } from '../share/shareLink';
|
||||
|
||||
export interface OrbitShareLink {
|
||||
/** Base URL of the Navidrome server (decoded). */
|
||||
|
||||
+7
-7
@@ -6,21 +6,21 @@
|
||||
* refactor (2026-05-12). This test pins the artist-first / random-fallback
|
||||
* order, the dedup contract against existingIds, and the autoAdded flag.
|
||||
*/
|
||||
import { getSimilarSongs2, getTopSongs } from '../api/subsonicArtists';
|
||||
import { getRandomSongs } from '../api/subsonicLibrary';
|
||||
import type { Track } from '../store/playerStoreTypes';
|
||||
import { getSimilarSongs2, getTopSongs } from '../../api/subsonicArtists';
|
||||
import { getRandomSongs } from '../../api/subsonicLibrary';
|
||||
import type { Track } from '../../store/playerStoreTypes';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('../api/subsonicArtists', () => ({
|
||||
vi.mock('../../api/subsonicArtists', () => ({
|
||||
getSimilarSongs2: vi.fn(),
|
||||
getTopSongs: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../api/subsonicLibrary', () => ({
|
||||
vi.mock('../../api/subsonicLibrary', () => ({
|
||||
getRandomSongs: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./mixRatingFilter', () => ({
|
||||
vi.mock('../mix/mixRatingFilter', () => ({
|
||||
getMixMinRatingsConfigFromAuth: vi.fn(),
|
||||
enrichSongsForMixRatingFilter: vi.fn(),
|
||||
passesMixMinRatings: vi.fn(),
|
||||
@@ -30,7 +30,7 @@ import { buildInfiniteQueueCandidates } from './buildInfiniteQueueCandidates';
|
||||
import {
|
||||
enrichSongsForMixRatingFilter,
|
||||
getMixMinRatingsConfigFromAuth,
|
||||
} from './mixRatingFilter';
|
||||
} from '../mix/mixRatingFilter';
|
||||
import { makeSubsonicSong } from '@/test/helpers/factories';
|
||||
|
||||
const seed = (overrides: Partial<Track> = {}): Track => ({
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
import { getSimilarSongs2, getTopSongs } from '../api/subsonicArtists';
|
||||
import { getRandomSongs } from '../api/subsonicLibrary';
|
||||
import type { Track } from '../store/playerStoreTypes';
|
||||
import { getSimilarSongs2, getTopSongs } from '../../api/subsonicArtists';
|
||||
import { getRandomSongs } from '../../api/subsonicLibrary';
|
||||
import type { Track } from '../../store/playerStoreTypes';
|
||||
import {
|
||||
enrichSongsForMixRatingFilter,
|
||||
getMixMinRatingsConfigFromAuth,
|
||||
passesMixMinRatings,
|
||||
} from './mixRatingFilter';
|
||||
} from '../mix/mixRatingFilter';
|
||||
import { shuffleArray } from './shuffleArray';
|
||||
import { songToTrack } from './songToTrack';
|
||||
/**
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getArtist } from '../api/subsonicArtists';
|
||||
import { getAlbum } from '../api/subsonicLibrary';
|
||||
import { getPlaylist } from '../api/subsonicPlaylists';
|
||||
import type { SubsonicSong } from '../api/subsonicTypes';
|
||||
import type { DeviceSyncSource } from '../store/deviceSyncStore';
|
||||
import { getArtist } from '../../api/subsonicArtists';
|
||||
import { getAlbum } from '../../api/subsonicLibrary';
|
||||
import { getPlaylist } from '../../api/subsonicPlaylists';
|
||||
import type { SubsonicSong } from '../../api/subsonicTypes';
|
||||
import type { DeviceSyncSource } from '../../store/deviceSyncStore';
|
||||
|
||||
export async function fetchTracksForSource(source: DeviceSyncSource): Promise<SubsonicSong[]> {
|
||||
if (source.type === 'playlist') { const { songs } = await getPlaylist(source.id); return songs; }
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getAlbum } from '../api/subsonicLibrary';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import { getAlbum } from '../../api/subsonicLibrary';
|
||||
import { usePlayerStore } from '../../store/playerStore';
|
||||
import { songToTrack } from './songToTrack';
|
||||
import { useOrbitStore } from '../store/orbitStore';
|
||||
import { useOrbitStore } from '../../store/orbitStore';
|
||||
|
||||
function fadeOut(setVolume: (v: number) => void, from: number, durationMs: number): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getArtist } from '../api/subsonicArtists';
|
||||
import { getAlbum } from '../api/subsonicLibrary';
|
||||
import { songToTrack } from '../utils/songToTrack';
|
||||
import { shuffleArray } from '../utils/shuffleArray';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import { getArtist } from '../../api/subsonicArtists';
|
||||
import { getAlbum } from '../../api/subsonicLibrary';
|
||||
import { songToTrack } from './songToTrack';
|
||||
import { shuffleArray } from './shuffleArray';
|
||||
import { usePlayerStore } from '../../store/playerStore';
|
||||
/**
|
||||
* All tracks from the artist’s albums, shuffled — same idea as Artist page “shuffle play”.
|
||||
*/
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getAlbum, getSong } from '../api/subsonicLibrary';
|
||||
import { songToTrack } from '../utils/songToTrack';
|
||||
import { getAlbum, getSong } from '../../api/subsonicLibrary';
|
||||
import { songToTrack } from './songToTrack';
|
||||
import { playAlbum } from './playAlbum';
|
||||
import { playArtistShuffled } from './playArtistShuffled';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import { usePlayerStore } from '../../store/playerStore';
|
||||
/**
|
||||
* `getSong` → `getAlbum` → `getArtist`: one opaque Subsonic id may refer to a track,
|
||||
* album, or artist depending on the server.
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SubsonicSong } from '../api/subsonicTypes';
|
||||
import { songToTrack } from '../utils/songToTrack';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import type { SubsonicSong } from '../../api/subsonicTypes';
|
||||
import { songToTrack } from './songToTrack';
|
||||
import { usePlayerStore } from '../../store/playerStore';
|
||||
function fadeOut(setVolume: (v: number) => void, from: number, durationMs: number): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
const steps = 16;
|
||||
@@ -4,7 +4,7 @@
|
||||
* routing through the HTTP source) and the no-op detection in
|
||||
* `queuesStructuralEqual` that prevents unnecessary store rewrites.
|
||||
*/
|
||||
import type { Track } from '../store/playerStoreTypes';
|
||||
import type { Track } from '../../store/playerStoreTypes';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
normalizeAnalysisTrackId,
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Track } from '../store/playerStoreTypes';
|
||||
import type { Track } from '../../store/playerStoreTypes';
|
||||
/**
|
||||
* Strip the `stream:` prefix that some Rust events attach to track ids when
|
||||
* they're routed through the HTTP source. Both forms identify the same track,
|
||||
@@ -1,6 +1,6 @@
|
||||
import { buildStreamUrl } from '../api/subsonicStreamUrl';
|
||||
import { useOfflineStore } from '../store/offlineStore';
|
||||
import { useHotCacheStore } from '../store/hotCacheStore';
|
||||
import { buildStreamUrl } from '../../api/subsonicStreamUrl';
|
||||
import { useOfflineStore } from '../../store/offlineStore';
|
||||
import { useHotCacheStore } from '../../store/hotCacheStore';
|
||||
|
||||
/** Same resolution order as {@link resolvePlaybackUrl} — for UI hints only. */
|
||||
export type PlaybackSourceKind = 'offline' | 'hot' | 'stream';
|
||||
@@ -5,7 +5,7 @@
|
||||
* playback queue. Originally lived in `playerStore.ts`; extracted in M0 of
|
||||
* the frontend refactor (2026-05-12).
|
||||
*/
|
||||
import type { Track } from '../store/playerStoreTypes';
|
||||
import type { Track } from '../../store/playerStoreTypes';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { songToTrack } from './songToTrack';
|
||||
import { makeSubsonicSong } from '@/test/helpers/factories';
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SubsonicSong } from '../api/subsonicTypes';
|
||||
import type { Track } from '../store/playerStoreTypes';
|
||||
import type { SubsonicSong } from '../../api/subsonicTypes';
|
||||
import type { Track } from '../../store/playerStoreTypes';
|
||||
export function songToTrack(song: SubsonicSong): Track {
|
||||
return {
|
||||
id: song.id,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { Track } from '../store/playerStoreTypes';
|
||||
import type { Track } from '../../store/playerStoreTypes';
|
||||
|
||||
export interface BulkPlayDeps {
|
||||
songsLength: number;
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SubsonicSong } from '../api/subsonicTypes';
|
||||
import type { SubsonicSong } from '../../api/subsonicTypes';
|
||||
|
||||
export type PlaylistSortKey = 'natural' | 'title' | 'artist' | 'album' | 'favorite' | 'rating' | 'duration';
|
||||
export type PlaylistSortDir = 'asc' | 'desc';
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { TFunction } from 'i18next';
|
||||
import { open as openDialog } from '@tauri-apps/plugin-dialog';
|
||||
import { readTextFile } from '@tauri-apps/plugin-fs';
|
||||
import { search } from '../api/subsonicSearch';
|
||||
import type { SubsonicSong } from '../api/subsonicTypes';
|
||||
import { showToast } from './toast';
|
||||
import { search } from '../../api/subsonicSearch';
|
||||
import type { SubsonicSong } from '../../api/subsonicTypes';
|
||||
import { showToast } from '../ui/toast';
|
||||
import { parseSpotifyCsv, type SpotifyCsvTrack } from './spotifyCsvImport';
|
||||
import {
|
||||
cleanTrackTitle,
|
||||
@@ -1,7 +1,7 @@
|
||||
import type React from 'react';
|
||||
import { getPlaylist } from '../api/subsonicPlaylists';
|
||||
import { filterSongsToActiveLibrary } from '../api/subsonicLibrary';
|
||||
import type { SubsonicPlaylist, SubsonicSong } from '../api/subsonicTypes';
|
||||
import { getPlaylist } from '../../api/subsonicPlaylists';
|
||||
import { filterSongsToActiveLibrary } from '../../api/subsonicLibrary';
|
||||
import type { SubsonicPlaylist, SubsonicSong } from '../../api/subsonicTypes';
|
||||
|
||||
export interface RunPlaylistLoadDeps {
|
||||
id: string;
|
||||
@@ -1,5 +1,5 @@
|
||||
import type React from 'react';
|
||||
import type { SubsonicSong } from '../api/subsonicTypes';
|
||||
import type { SubsonicSong } from '../../api/subsonicTypes';
|
||||
|
||||
export interface RunPlaylistReorderDropDeps {
|
||||
e: Event;
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { TFunction } from 'i18next';
|
||||
import { getPlaylist, updatePlaylistMeta, uploadPlaylistCoverArt } from '../api/subsonicPlaylists';
|
||||
import type { SubsonicPlaylist } from '../api/subsonicTypes';
|
||||
import { showToast } from './toast';
|
||||
import { getPlaylist, updatePlaylistMeta, uploadPlaylistCoverArt } from '../../api/subsonicPlaylists';
|
||||
import type { SubsonicPlaylist } from '../../api/subsonicTypes';
|
||||
import { showToast } from '../ui/toast';
|
||||
|
||||
export interface RunPlaylistSaveMetaDeps {
|
||||
id: string;
|
||||
@@ -1,9 +1,9 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { join } from '@tauri-apps/api/path';
|
||||
import { buildDownloadUrl } from '../api/subsonicStreamUrl';
|
||||
import type { SubsonicPlaylist } from '../api/subsonicTypes';
|
||||
import { useZipDownloadStore } from '../store/zipDownloadStore';
|
||||
import { sanitizeFilename } from './playlistDetailHelpers';
|
||||
import { buildDownloadUrl } from '../../api/subsonicStreamUrl';
|
||||
import type { SubsonicPlaylist } from '../../api/subsonicTypes';
|
||||
import { useZipDownloadStore } from '../../store/zipDownloadStore';
|
||||
import { sanitizeFilename } from '../componentHelpers/playlistDetailHelpers';
|
||||
|
||||
export interface RunPlaylistZipDownloadDeps {
|
||||
playlist: SubsonicPlaylist;
|
||||
@@ -1,9 +1,9 @@
|
||||
import type React from 'react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { deletePlaylist, getPlaylist, updatePlaylist } from '../api/subsonicPlaylists';
|
||||
import type { SubsonicPlaylist } from '../api/subsonicTypes';
|
||||
import { usePlaylistStore } from '../store/playlistStore';
|
||||
import { showToast } from './toast';
|
||||
import { deletePlaylist, getPlaylist, updatePlaylist } from '../../api/subsonicPlaylists';
|
||||
import type { SubsonicPlaylist } from '../../api/subsonicTypes';
|
||||
import { usePlaylistStore } from '../../store/playlistStore';
|
||||
import { showToast } from '../ui/toast';
|
||||
|
||||
export interface RunPlaylistDeleteDeps {
|
||||
e: React.MouseEvent;
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
import type React from 'react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { ndGetSmartPlaylist, ndListSmartPlaylists } from '../api/navidromeSmart';
|
||||
import type { SubsonicPlaylist } from '../api/subsonicTypes';
|
||||
import { ndGetSmartPlaylist, ndListSmartPlaylists } from '../../api/navidromeSmart';
|
||||
import type { SubsonicPlaylist } from '../../api/subsonicTypes';
|
||||
import {
|
||||
defaultSmartFilters, displayPlaylistName, isSmartPlaylistName,
|
||||
parseSmartRulesToFilters, type SmartFilters,
|
||||
} from './playlistsSmart';
|
||||
import { showToast } from './toast';
|
||||
import { showToast } from '../ui/toast';
|
||||
|
||||
export interface RunPlaylistsOpenSmartEditorDeps {
|
||||
pl: SubsonicPlaylist;
|
||||
@@ -1,13 +1,13 @@
|
||||
import type React from 'react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { ndCreateSmartPlaylist, ndUpdateSmartPlaylist } from '../api/navidromeSmart';
|
||||
import type { SubsonicPlaylist } from '../api/subsonicTypes';
|
||||
import { usePlaylistStore } from '../store/playlistStore';
|
||||
import { ndCreateSmartPlaylist, ndUpdateSmartPlaylist } from '../../api/navidromeSmart';
|
||||
import type { SubsonicPlaylist } from '../../api/subsonicTypes';
|
||||
import { usePlaylistStore } from '../../store/playlistStore';
|
||||
import {
|
||||
buildSmartRulesPayload, defaultSmartFilters, SMART_PREFIX,
|
||||
type PendingSmartPlaylist, type SmartFilters,
|
||||
} from './playlistsSmart';
|
||||
import { showToast } from './toast';
|
||||
import { showToast } from '../ui/toast';
|
||||
|
||||
export interface RunPlaylistsSaveSmartDeps {
|
||||
isNavidromeServer: boolean;
|
||||
@@ -1,6 +1,6 @@
|
||||
import type React from 'react';
|
||||
import type { SubsonicSong } from '../api/subsonicTypes';
|
||||
import { songToTrack } from './songToTrack';
|
||||
import type { SubsonicSong } from '../../api/subsonicTypes';
|
||||
import { songToTrack } from '../playback/songToTrack';
|
||||
|
||||
export interface StartPlaylistRowDragDeps {
|
||||
e: React.MouseEvent;
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ServerProfile } from '../store/authStoreTypes';
|
||||
import type { ServerProfile } from '../../store/authStoreTypes';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { serverListDisplayLabel, shortHostFromServerUrl } from './serverDisplayName';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ServerProfile } from '../store/authStoreTypes';
|
||||
import type { ServerProfile } from '../../store/authStoreTypes';
|
||||
/** Host (+ port) from a server base URL, e.g. `https://music.one.com/foo` → `music.one.com`. */
|
||||
export function shortHostFromServerUrl(urlRaw: string): string {
|
||||
const t = urlRaw.trim();
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ServerProfile } from '../store/authStoreTypes';
|
||||
import { pingWithCredentials, scheduleInstantMixProbeForServer } from '../api/subsonic';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { useOrbitStore } from '../store/orbitStore';
|
||||
import { endOrbitSession, leaveOrbitSession } from './orbit';
|
||||
import type { ServerProfile } from '../../store/authStoreTypes';
|
||||
import { pingWithCredentials, scheduleInstantMixProbeForServer } from '../../api/subsonic';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { useOrbitStore } from '../../store/orbitStore';
|
||||
import { endOrbitSession, leaveOrbitSession } from '../orbit';
|
||||
|
||||
export async function switchActiveServer(server: ServerProfile): Promise<boolean> {
|
||||
try {
|
||||
@@ -1,13 +1,13 @@
|
||||
import { getArtist } from '../api/subsonicArtists';
|
||||
import { getAlbum, getSong } from '../api/subsonicLibrary';
|
||||
import type { SubsonicSong } from '../api/subsonicTypes';
|
||||
import { songToTrack } from '../utils/songToTrack';
|
||||
import { getArtist } from '../../api/subsonicArtists';
|
||||
import { getAlbum, getSong } from '../../api/subsonicLibrary';
|
||||
import type { SubsonicSong } from '../../api/subsonicTypes';
|
||||
import { songToTrack } from '../playback/songToTrack';
|
||||
import type { NavigateFunction } from 'react-router-dom';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { usePlayerStore } from '../../store/playerStore';
|
||||
import { findServerIdForShareUrl, type EntitySharePayloadV1 } from './shareLink';
|
||||
import { showToast } from './toast';
|
||||
import { showToast } from '../ui/toast';
|
||||
|
||||
const RESOLVE_QUEUE_CHUNK = 12;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { encodeSharePayload, type EntityShareKind } from './shareLink';
|
||||
import { copyTextToClipboard } from './serverMagicString';
|
||||
import { copyTextToClipboard } from '../server/serverMagicString';
|
||||
|
||||
/** Copies a track / album / artist / composer share link (`psysonic2-`) to the clipboard. */
|
||||
export async function copyEntityShareLink(kind: EntityShareKind, id: string): Promise<boolean> {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user