mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 23:35:44 +00:00
refactor(share): split share cluster into lib/share + features/share
a11y-branch hold lifted. Pure share machinery (no @/features runtime imports) → lib/share: shareLink, shareSearch, shareServerOriginLabel, copyEntityShareLink (+tests). The two orchestrators that runtime-import offline/playback/orbit → new features/share: applySharePaste, enqueueShareSearchPayload (+test); doc-only barrel, consumers use deep paths. No low-layer consumes the orchestrators, so no seam is needed — pure move, tests pass unmodified. Drains utils/share. Only import lines changed in the PasteClipboardHandler consumer (logic untouched).
This commit is contained in:
@@ -2,11 +2,11 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import type { EntitySharePayloadV1 } from '../utils/share/shareLink';
|
||||
import { decodeSharePayloadFromText } from '../utils/share/shareLink';
|
||||
import type { EntitySharePayloadV1 } from '@/lib/share/shareLink';
|
||||
import { decodeSharePayloadFromText } from '@/lib/share/shareLink';
|
||||
import { decodeServerMagicStringFromText } from '@/lib/server/serverMagicString';
|
||||
import { applySharePastePayload, applySharePasteQueue } from '../utils/share/applySharePaste';
|
||||
import { shareQueueServerContext } from '../utils/share/shareServerOriginLabel';
|
||||
import { applySharePastePayload, applySharePasteQueue } from '@/features/share/applySharePaste';
|
||||
import { shareQueueServerContext } from '@/lib/share/shareServerOriginLabel';
|
||||
import { showToast } from '@/lib/dom/toast';
|
||||
import { useShareQueuePreview } from '@/features/search';
|
||||
import { ShareQueuePreviewModal } from '@/features/search';
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useIsMobile } from '@/lib/hooks/useIsMobile';
|
||||
import { useAlbumDetailBack } from '@/features/album/hooks/useAlbumDetailBack';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import StarRating from '@/ui/StarRating';
|
||||
import { copyEntityShareLink } from '@/utils/share/copyEntityShareLink';
|
||||
import { copyEntityShareLink } from '@/lib/share/copyEntityShareLink';
|
||||
import { showToast } from '@/lib/dom/toast';
|
||||
import { isAlbumRecentlyAdded } from '@/features/album/utils/albumRecency';
|
||||
import { formatLongDuration } from '@/lib/format/formatDuration';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { uploadArtistImage } from '@/lib/api/subsonicArtists';
|
||||
import { setRating, star, unstar } from '@/lib/api/subsonicStarRating';
|
||||
import type { SubsonicArtist } from '@/lib/api/subsonicTypes';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { copyEntityShareLink } from '@/utils/share/copyEntityShareLink';
|
||||
import { copyEntityShareLink } from '@/lib/share/copyEntityShareLink';
|
||||
import { invalidateCoverArt } from '@/utils/imageCache';
|
||||
import { showToast } from '@/lib/dom/toast';
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { open } from '@tauri-apps/plugin-shell';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { copyEntityShareLink } from '@/utils/share/copyEntityShareLink';
|
||||
import { copyEntityShareLink } from '@/lib/share/copyEntityShareLink';
|
||||
import { showToast } from '@/lib/dom/toast';
|
||||
import { sanitizeHtml } from '@/lib/util/sanitizeHtml';
|
||||
import { usePerfProbeFlags } from '@/lib/perf/perfFlags';
|
||||
|
||||
@@ -5,7 +5,7 @@ import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { EntityShareKind } from '@/utils/share/shareLink';
|
||||
import type { EntityShareKind } from '@/lib/share/shareLink';
|
||||
import { AddToPlaylistSubmenu } from '@/features/contextMenu/components/AddToPlaylistSubmenu';
|
||||
import {
|
||||
copyShareLink as copyShareLinkAction,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type React from 'react';
|
||||
import type { SubsonicAlbum, SubsonicArtist } from '@/lib/api/subsonicTypes';
|
||||
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
|
||||
import type { EntityShareKind } from '@/utils/share/shareLink';
|
||||
import type { EntityShareKind } from '@/lib/share/shareLink';
|
||||
import type { OfflineActionPolicy } from '@/features/offline';
|
||||
|
||||
export type RatingKind = 'song' | 'album' | 'artist';
|
||||
|
||||
@@ -9,8 +9,8 @@ import type { Track } from '@/lib/media/trackTypes';
|
||||
import { resolveQueueTrack } from '@/features/playback/store/queueTrackView';
|
||||
import { useZipDownloadStore } from '@/features/offline';
|
||||
import { useDownloadModalStore } from '@/features/offline';
|
||||
import type { EntityShareKind } from '@/utils/share/shareLink';
|
||||
import { copyEntityShareLink } from '@/utils/share/copyEntityShareLink';
|
||||
import type { EntityShareKind } from '@/lib/share/shareLink';
|
||||
import { copyEntityShareLink } from '@/lib/share/copyEntityShareLink';
|
||||
import { sanitizeFilename, shuffleArray } from '@/features/contextMenu/utils/contextMenuHelpers';
|
||||
import { songToTrack } from '@/lib/media/songToTrack';
|
||||
import { showToast } from '@/lib/dom/toast';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { decodeOrbitSharePayloadFromText, encodeSharePayload } from '@/utils/share/shareLink';
|
||||
import { decodeOrbitSharePayloadFromText, encodeSharePayload } from '@/lib/share/shareLink';
|
||||
|
||||
export interface OrbitShareLink {
|
||||
/** Base URL of the Navidrome server (decoded). */
|
||||
|
||||
@@ -12,7 +12,7 @@ import { usePlaylistStore } from '@/features/playlist';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePlaybackLibraryNavigate } from '@/features/playback/hooks/usePlaybackLibraryNavigate';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { encodeSharePayload } from '@/utils/share/shareLink';
|
||||
import { encodeSharePayload } from '@/lib/share/shareLink';
|
||||
import { serverShareBaseUrl } from '@/lib/server/serverEndpoint';
|
||||
import { copyTextToClipboard } from '@/lib/server/serverMagicString';
|
||||
import { showToast } from '@/lib/dom/toast';
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { SubsonicSong } from '@/lib/api/subsonicTypes';
|
||||
import type { ServerProfile } from '@/store/authStoreTypes';
|
||||
import { formatTrackTime } from '@/lib/format/formatDuration';
|
||||
import type { ShareQueuePreviewState } from '@/features/search/hooks/useShareQueuePreview';
|
||||
import { sharePayloadTotal, type QueueableShareSearchPayload } from '@/utils/share/shareSearch';
|
||||
import { sharePayloadTotal, type QueueableShareSearchPayload } from '@/lib/share/shareSearch';
|
||||
import OverlayScrollArea from '@/ui/OverlayScrollArea';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import { COVER_DENSE_SEARCH_CSS_PX } from '@/cover/layoutSizes';
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { TFunction } from 'i18next';
|
||||
import type { SubsonicArtist } from '@/lib/api/subsonicTypes';
|
||||
import type { ServerProfile } from '@/store/authStoreTypes';
|
||||
import { songToTrack } from '@/lib/media/songToTrack';
|
||||
import { activateShareSearchServer } from '@/utils/share/enqueueShareSearchPayload';
|
||||
import { sharePayloadTotal, type ShareSearchMatch } from '@/utils/share/shareSearch';
|
||||
import { activateShareSearchServer } from '@/features/share/enqueueShareSearchPayload';
|
||||
import { sharePayloadTotal, type ShareSearchMatch } from '@/lib/share/shareSearch';
|
||||
import type { ShareSearchPreviewState } from '@/features/search/hooks/useShareSearchPreview';
|
||||
import { FETCH_QUEUE_BIAS_SEARCH_ARTIST_OVER_ALBUM } from '@/ui/CachedImage';
|
||||
import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage';
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { SubsonicSong } from '@/lib/api/subsonicTypes';
|
||||
import {
|
||||
resolveShareSearchPayload,
|
||||
type ShareSearchResolveResult,
|
||||
} from '@/utils/share/enqueueShareSearchPayload';
|
||||
import type { QueueableShareSearchPayload } from '@/utils/share/shareSearch';
|
||||
} from '@/features/share/enqueueShareSearchPayload';
|
||||
import type { QueueableShareSearchPayload } from '@/lib/share/shareSearch';
|
||||
|
||||
export type ShareQueuePreviewState =
|
||||
| { status: 'idle' }
|
||||
|
||||
@@ -6,11 +6,11 @@ import { useAuthStore } from '@/store/authStore';
|
||||
import {
|
||||
activateShareSearchServer,
|
||||
enqueueShareSearchPayload,
|
||||
} from '@/utils/share/enqueueShareSearchPayload';
|
||||
} from '@/features/share/enqueueShareSearchPayload';
|
||||
import type { ServerProfile } from '@/store/authStoreTypes';
|
||||
import { findServerIdForShareUrl } from '@/utils/share/shareLink';
|
||||
import { shareServerOriginLabel } from '@/utils/share/shareServerOriginLabel';
|
||||
import { parseShareSearchText } from '@/utils/share/shareSearch';
|
||||
import { findServerIdForShareUrl } from '@/lib/share/shareLink';
|
||||
import { shareServerOriginLabel } from '@/lib/share/shareServerOriginLabel';
|
||||
import { parseShareSearchText } from '@/lib/share/shareSearch';
|
||||
import { serverIndexKeyFromUrl } from '@/lib/server/serverIndexKey';
|
||||
import { useShareSearchPreview } from '@/features/search/hooks/useShareSearchPreview';
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
resolveShareSearchAlbum,
|
||||
resolveShareSearchArtist,
|
||||
resolveShareSearchPayload,
|
||||
} from '@/utils/share/enqueueShareSearchPayload';
|
||||
import type { ShareSearchMatch } from '@/utils/share/shareSearch';
|
||||
} from '@/features/share/enqueueShareSearchPayload';
|
||||
import type { ShareSearchMatch } from '@/lib/share/shareSearch';
|
||||
|
||||
export interface ShareSearchPreviewState {
|
||||
shareTrackSong: SubsonicSong | null;
|
||||
|
||||
@@ -4,10 +4,10 @@ import type { SubsonicSong } from '@/lib/api/subsonicTypes';
|
||||
import { songToTrack } from '@/lib/media/songToTrack';
|
||||
import type { Location, NavigateFunction } from 'react-router-dom';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import { navigateToAlbumDetail } from '@/lib/navigation/albumDetailNavigation';
|
||||
import { findServerIdForShareUrl, type EntitySharePayloadV1 } from './shareLink';
|
||||
import { findServerIdForShareUrl, type EntitySharePayloadV1 } from '@/lib/share/shareLink';
|
||||
import { showToast } from '@/lib/dom/toast';
|
||||
|
||||
const RESOLVE_QUEUE_CHUNK = 12;
|
||||
+2
-2
@@ -37,7 +37,7 @@ vi.mock('@/lib/api/subsonicEntityWithCredentials', () => ({
|
||||
getSongWithCredentials: mocks.getSongWithCredentials,
|
||||
}));
|
||||
|
||||
vi.mock('../../store/authStore', () => ({
|
||||
vi.mock('@/store/authStore', () => ({
|
||||
useAuthStore: {
|
||||
getState: () => mocks.authState.current,
|
||||
},
|
||||
@@ -67,7 +67,7 @@ import {
|
||||
resolveShareSearchAlbum,
|
||||
resolveShareSearchArtist,
|
||||
resolveShareSearchPayload,
|
||||
} from './enqueueShareSearchPayload';
|
||||
} from '@/features/share/enqueueShareSearchPayload';
|
||||
|
||||
const sharedServer = {
|
||||
id: 'shared',
|
||||
+4
-4
@@ -7,13 +7,13 @@ import {
|
||||
import { getSong } from '@/lib/api/subsonicLibrary';
|
||||
import { resolveAlbum, resolveArtist } from '@/features/offline';
|
||||
import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '@/lib/api/subsonicTypes';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import type { ServerProfile } from '../../store/authStoreTypes';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import type { ServerProfile } from '@/store/authStoreTypes';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import { songToTrack } from '@/lib/media/songToTrack';
|
||||
import type { Track } from '@/lib/media/trackTypes';
|
||||
import { orbitBulkGuard } from '@/features/orbit';
|
||||
import { findServerIdForShareUrl } from './shareLink';
|
||||
import { findServerIdForShareUrl } from '@/lib/share/shareLink';
|
||||
import { connectBaseUrlForServer } from '@/lib/server/serverEndpoint';
|
||||
import { serverIndexKeyFromUrl } from '@/lib/server/serverIndexKey';
|
||||
import type {
|
||||
@@ -21,7 +21,7 @@ import type {
|
||||
ArtistShareSearchPayload,
|
||||
ComposerShareSearchPayload,
|
||||
QueueableShareSearchPayload,
|
||||
} from './shareSearch';
|
||||
} from '@/lib/share/shareSearch';
|
||||
import { showToast } from '@/lib/dom/toast';
|
||||
|
||||
const RESOLVE_QUEUE_CHUNK = 12;
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Share feature — applying pasted entity/queue shares (server switch + resolve +
|
||||
* play/navigate) and enqueueing share-search results. The pure encode/decode/parse
|
||||
* and origin-label helpers live in `@/lib/share` (feature-free); these orchestrators
|
||||
* runtime-import offline/playback/orbit, so they are feature-scoped. Consumers import
|
||||
* deep paths (`@/features/share/...`); this barrel is documentation.
|
||||
*/
|
||||
export * from './applySharePaste';
|
||||
export * from './enqueueShareSearchPayload';
|
||||
+2
-2
@@ -7,11 +7,11 @@
|
||||
* garbage to the clipboard.
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { copyEntityShareLink } from './copyEntityShareLink';
|
||||
import { copyEntityShareLink } from '@/lib/share/copyEntityShareLink';
|
||||
import {
|
||||
decodeSharePayloadFromText,
|
||||
PSYSONIC_SHARE_PREFIX,
|
||||
} from './shareLink';
|
||||
} from '@/lib/share/shareLink';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { resetAuthStore } from '@/test/helpers/storeReset';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { serverShareBaseUrl } from '@/lib/server/serverEndpoint';
|
||||
import { encodeSharePayload, type EntityShareKind } from './shareLink';
|
||||
import { encodeSharePayload, type EntityShareKind } from '@/lib/share/shareLink';
|
||||
import { copyTextToClipboard } from '@/lib/server/serverMagicString';
|
||||
|
||||
/** Copies a track / album / artist / composer share link (`psysonic2-`) to the clipboard. */
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
encodeSharePayload,
|
||||
findServerIdForShareUrl,
|
||||
normalizeShareServerUrl,
|
||||
} from './shareLink';
|
||||
} from '@/lib/share/shareLink';
|
||||
import { decodeServerMagicString, encodeServerMagicString, SERVER_MAGIC_STRING_PREFIX } from '@/lib/server/serverMagicString';
|
||||
import { makeServer } from '@/test/helpers/factories';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ServerProfile } from '../../store/authStoreTypes';
|
||||
import type { ServerProfile } from '@/store/authStoreTypes';
|
||||
/** Library share (track / album / artist / queue). Same naming family as `psysonic1-` server invites. */
|
||||
export const PSYSONIC_SHARE_PREFIX = 'psysonic2-';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { encodeServerMagicString } from '@/lib/server/serverMagicString';
|
||||
import { encodeSharePayload } from './shareLink';
|
||||
import { parseShareSearchText, sharePayloadTotal } from './shareSearch';
|
||||
import { encodeSharePayload } from '@/lib/share/shareLink';
|
||||
import { parseShareSearchText, sharePayloadTotal } from '@/lib/share/shareSearch';
|
||||
|
||||
describe('share search parsing', () => {
|
||||
it('detects track share links as queueable', () => {
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
decodeSharePayloadFromText,
|
||||
PSYSONIC_SHARE_PREFIX,
|
||||
} from './shareLink';
|
||||
} from '@/lib/share/shareLink';
|
||||
|
||||
export type QueueableShareSearchPayload =
|
||||
| { srv: string; k: 'track'; id: string }
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { encodeSharePayload } from './shareLink';
|
||||
import { parseShareSearchText } from './shareSearch';
|
||||
import { shareServerOriginLabel } from './shareServerOriginLabel';
|
||||
import { encodeSharePayload } from '@/lib/share/shareLink';
|
||||
import { parseShareSearchText } from '@/lib/share/shareSearch';
|
||||
import { shareServerOriginLabel } from '@/lib/share/shareServerOriginLabel';
|
||||
|
||||
const home = {
|
||||
id: 'home',
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ServerProfile } from '../../store/authStoreTypes';
|
||||
import type { ServerProfile } from '@/store/authStoreTypes';
|
||||
import { serverListDisplayLabel } from '@/lib/server/serverDisplayName';
|
||||
import { serverIndexKeyFromUrl } from '@/lib/server/serverIndexKey';
|
||||
import { findServerIdForShareUrl } from './shareLink';
|
||||
import type { ShareSearchMatch } from './shareSearch';
|
||||
import { findServerIdForShareUrl } from '@/lib/share/shareLink';
|
||||
import type { ShareSearchMatch } from '@/lib/share/shareSearch';
|
||||
|
||||
/**
|
||||
* Display name for the share link's origin server when it differs from the
|
||||
Reference in New Issue
Block a user