morille 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +82 -0
  2. package/dist/app.d.ts +5 -0
  3. package/dist/app.js +66 -0
  4. package/dist/components/animated-line.d.ts +12 -0
  5. package/dist/components/animated-line.js +14 -0
  6. package/dist/components/browse-detail-view.d.ts +14 -0
  7. package/dist/components/browse-detail-view.js +31 -0
  8. package/dist/components/keyboard-hints.d.ts +12 -0
  9. package/dist/components/keyboard-hints.js +8 -0
  10. package/dist/components/lyrics-panel.d.ts +10 -0
  11. package/dist/components/lyrics-panel.js +38 -0
  12. package/dist/components/lyrics-view.d.ts +14 -0
  13. package/dist/components/lyrics-view.js +50 -0
  14. package/dist/components/panel-content.d.ts +10 -0
  15. package/dist/components/panel-content.js +22 -0
  16. package/dist/components/playback-status.d.ts +16 -0
  17. package/dist/components/playback-status.js +22 -0
  18. package/dist/components/player.d.ts +9 -0
  19. package/dist/components/player.d.ts.map +1 -0
  20. package/dist/components/player.js +215 -0
  21. package/dist/components/player.js.map +1 -0
  22. package/dist/components/progress-bar.d.ts +11 -0
  23. package/dist/components/progress-bar.js +13 -0
  24. package/dist/components/queue-view.d.ts +9 -0
  25. package/dist/components/queue-view.js +54 -0
  26. package/dist/components/search-panel.d.ts +8 -0
  27. package/dist/components/search-panel.js +152 -0
  28. package/dist/components/shimmer.d.ts +12 -0
  29. package/dist/components/shimmer.js +34 -0
  30. package/dist/components/side-panel.d.ts +12 -0
  31. package/dist/components/side-panel.js +12 -0
  32. package/dist/components/track-info-skeleton.d.ts +9 -0
  33. package/dist/components/track-info-skeleton.js +10 -0
  34. package/dist/components/track-info.d.ts +10 -0
  35. package/dist/components/track-info.js +15 -0
  36. package/dist/config.d.ts +33 -0
  37. package/dist/config.d.ts.map +1 -0
  38. package/dist/config.js +65 -0
  39. package/dist/config.js.map +1 -0
  40. package/dist/contexts/lyrics-context.d.ts +29 -0
  41. package/dist/contexts/lyrics-context.js +44 -0
  42. package/dist/contexts/panel-mode-context.d.ts +24 -0
  43. package/dist/contexts/panel-mode-context.js +45 -0
  44. package/dist/contexts/queue-context.d.ts +32 -0
  45. package/dist/contexts/queue-context.js +68 -0
  46. package/dist/contexts/search-context.d.ts +59 -0
  47. package/dist/contexts/search-context.js +338 -0
  48. package/dist/hooks/use-album-art.d.ts +8 -0
  49. package/dist/hooks/use-album-art.js +56 -0
  50. package/dist/hooks/use-browse.d.ts +29 -0
  51. package/dist/hooks/use-browse.js +98 -0
  52. package/dist/hooks/use-lyrics.d.ts +12 -0
  53. package/dist/hooks/use-lyrics.js +51 -0
  54. package/dist/hooks/use-playback.d.ts +24 -0
  55. package/dist/hooks/use-playback.js +282 -0
  56. package/dist/hooks/use-player-input.d.ts +18 -0
  57. package/dist/hooks/use-player-input.js +201 -0
  58. package/dist/hooks/use-queue.d.ts +28 -0
  59. package/dist/hooks/use-queue.js +194 -0
  60. package/dist/hooks/use-search.d.ts +16 -0
  61. package/dist/hooks/use-search.js +77 -0
  62. package/dist/index.d.ts +3 -0
  63. package/dist/index.js +10 -0
  64. package/dist/main.d.ts +1 -0
  65. package/dist/main.js +6 -0
  66. package/dist/spotify/auth.d.ts +36 -0
  67. package/dist/spotify/auth.js +183 -0
  68. package/dist/spotify/client.d.ts +18 -0
  69. package/dist/spotify/client.js +48 -0
  70. package/dist/spotify/fetch-with-retry.d.ts +6 -0
  71. package/dist/spotify/fetch-with-retry.js +70 -0
  72. package/dist/spotify/lyrics.d.ts +25 -0
  73. package/dist/spotify/lyrics.js +130 -0
  74. package/dist/spotify/playback.d.ts +115 -0
  75. package/dist/spotify/playback.js +201 -0
  76. package/dist/spotify/search.d.ts +79 -0
  77. package/dist/spotify/search.js +143 -0
  78. package/package.json +33 -0
package/dist/config.js ADDED
@@ -0,0 +1,65 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ /**
6
+ * Resolves package.json at runtime so the `name`, `version`, and `homepage`
7
+ * fields can flow into user-visible strings (User-Agent, about dialog).
8
+ * Works both from source (`src/config.ts` → `../package.json`) and from the
9
+ * compiled binary (`dist/config.js` → `../package.json`) since the relative
10
+ * depth is the same.
11
+ */
12
+ function loadPackageInfo() {
13
+ const here = dirname(fileURLToPath(import.meta.url));
14
+ const pkgPath = join(here, '..', 'package.json');
15
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
16
+ return {
17
+ name: pkg.name,
18
+ version: pkg.version,
19
+ homepage: pkg.homepage,
20
+ };
21
+ }
22
+ const PKG = loadPackageInfo();
23
+ /**
24
+ * Default Spotify Client ID embedded in the binary.
25
+ * Users can override this at runtime via the `SPOTIFY_CLIENT_ID` environment variable.
26
+ * Leave empty to force users to provide their own Client ID.
27
+ */
28
+ export const DEFAULT_SPOTIFY_CLIENT_ID = '396cd16a363947ad95cb1032acdc4fc8';
29
+ /**
30
+ * User-Agent sent with every outgoing HTTP request (Spotify Web API + LRCLIB).
31
+ * Generated from package.json at runtime so it automatically tracks name/version.
32
+ * Shared via the `fetchWithRetry` wrapper which injects it into all requests.
33
+ */
34
+ export const USER_AGENT = PKG.homepage ? `${PKG.name}/${PKG.version} (${PKG.homepage})` : `${PKG.name}/${PKG.version}`;
35
+ // @see: https://developer.spotify.com/documentation/web-api/concepts/scopes
36
+ export const AUTH_SCOPES = [
37
+ 'user-read-playback-state',
38
+ 'user-modify-playback-state',
39
+ 'user-read-currently-playing',
40
+ 'user-read-recently-played',
41
+ 'user-library-read',
42
+ 'user-library-modify',
43
+ 'playlist-read-private',
44
+ 'playlist-read-collaborative',
45
+ ].join(' ');
46
+ export const AUTH_REDIRECT_PORT = 8888;
47
+ export const AUTH_REDIRECT_URI = `http://127.0.0.1:${AUTH_REDIRECT_PORT}/callback`;
48
+ export const CONFIG_DIR = join(homedir(), '.config', 'morille');
49
+ export const TOKEN_PATH = join(CONFIG_DIR, 'tokens.json');
50
+ export const POLL_INTERVAL_MS = 10_000;
51
+ export const TICK_INTERVAL_MS = 33; // 30 fps
52
+ export const SEEK_STEP_MS = 10_000;
53
+ export const VOLUME_STEP = 5;
54
+ export const LYRICS_WIDE_COLUMNS = 80;
55
+ export const LYRICS_DEFAULT_OFFSET_MS = 100;
56
+ export const LYRICS_OFFSET_STEP_MS = 100;
57
+ export const SEARCH_DEBOUNCE_MS = 300;
58
+ /**
59
+ * Number of results returned per category by the search endpoint.
60
+ * Spotify documents max=50, but multi-type searches (track+album+artist+playlist)
61
+ * have an undocumented stricter cap that rejects larger values with
62
+ * `400 Invalid limit`. 20 is the documented default and is reliably accepted.
63
+ */
64
+ export const SEARCH_RESULTS_LIMIT = 20;
65
+ export const USER_PLAYLISTS_LIMIT = 50;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC;;;;;;GAMG;AACH,SAAS,eAAe;IAKtB,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACrD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IACvD,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,QAAQ,EAAE,GAAG,CAAC,QAAQ;KACvB,CAAC;AACJ,CAAC;AAED,MAAM,GAAG,GAAG,eAAe,EAAE,CAAC;AAE9B;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,kCAAkC,CAAC;AAE5E;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,OAAO,KAAK,GAAG,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;AAEvH,4EAA4E;AAC5E,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,0BAA0B;IAC1B,4BAA4B;IAC5B,6BAA6B;IAC7B,2BAA2B;IAC3B,mBAAmB;IACnB,qBAAqB;IACrB,uBAAuB;IACvB,6BAA6B;CAC9B,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACZ,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAC;AACvC,MAAM,CAAC,MAAM,iBAAiB,GAAG,oBAAoB,kBAAkB,WAAW,CAAC;AAEnF,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;AAChE,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AAE1D,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC;AACvC,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC,CAAC,SAAS;AAC7C,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC;AACnC,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAC7B,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AACtC,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,CAAC;AAC5C,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AACzC,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AACtC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAC;AACvC,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAC"}
@@ -0,0 +1,29 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { Lyrics } from '../spotify/lyrics.js';
3
+ import type { TrackInfo } from '../spotify/playback.js';
4
+ type LyricsContextValue = {
5
+ lyrics: Lyrics | null;
6
+ isLoading: boolean;
7
+ offset: number;
8
+ durationMs: number;
9
+ scrollPosition: number;
10
+ isPlainText: boolean;
11
+ offsetUp: (() => void) | undefined;
12
+ offsetDown: (() => void) | undefined;
13
+ scrollUp: (() => void) | undefined;
14
+ scrollDown: (() => void) | undefined;
15
+ };
16
+ type LyricsProviderProps = {
17
+ track: TrackInfo | null;
18
+ children: ReactNode;
19
+ };
20
+ /**
21
+ * Provides lyrics data and offset controls.
22
+ * Only fetches lyrics when the lyrics panel is active.
23
+ */
24
+ export declare function LyricsProvider({ track, children }: LyricsProviderProps): import("react/jsx-runtime").JSX.Element;
25
+ /**
26
+ * Access lyrics data and offset controls.
27
+ */
28
+ export declare function useLyricsContext(): LyricsContextValue;
29
+ export {};
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useContext, useState } from 'react';
3
+ import { LYRICS_DEFAULT_OFFSET_MS, LYRICS_OFFSET_STEP_MS } from '../config.js';
4
+ import { useLyrics } from '../hooks/use-lyrics.js';
5
+ import { usePanelMode } from './panel-mode-context.js';
6
+ const LyricsContext = createContext(null);
7
+ /**
8
+ * Provides lyrics data and offset controls.
9
+ * Only fetches lyrics when the lyrics panel is active.
10
+ */
11
+ export function LyricsProvider({ track, children }) {
12
+ const { panelMode } = usePanelMode();
13
+ const isActive = panelMode === 'lyrics';
14
+ const { lyrics, isLoading } = useLyrics(isActive ? track : null);
15
+ const [offset, setOffset] = useState(LYRICS_DEFAULT_OFFSET_MS);
16
+ const [scrollPosition, setScrollPosition] = useState(0);
17
+ const isPlainText = !!lyrics && lyrics.synced.length === 0 && !!lyrics.plain;
18
+ const offsetUp = useCallback(() => setOffset((v) => v + LYRICS_OFFSET_STEP_MS), []);
19
+ const offsetDown = useCallback(() => setOffset((v) => v - LYRICS_OFFSET_STEP_MS), []);
20
+ const scrollUp = useCallback(() => setScrollPosition((v) => Math.max(0, v - 1)), []);
21
+ const scrollDown = useCallback(() => setScrollPosition((v) => v + 1), []);
22
+ const value = {
23
+ lyrics,
24
+ isLoading,
25
+ offset,
26
+ durationMs: track?.durationMs ?? 0,
27
+ scrollPosition,
28
+ isPlainText,
29
+ offsetUp: isActive && !isPlainText ? offsetUp : undefined,
30
+ offsetDown: isActive && !isPlainText ? offsetDown : undefined,
31
+ scrollUp: isActive && isPlainText ? scrollUp : undefined,
32
+ scrollDown: isActive && isPlainText ? scrollDown : undefined,
33
+ };
34
+ return _jsx(LyricsContext, { value: value, children: children });
35
+ }
36
+ /**
37
+ * Access lyrics data and offset controls.
38
+ */
39
+ export function useLyricsContext() {
40
+ const ctx = useContext(LyricsContext);
41
+ if (!ctx)
42
+ throw new Error('useLyricsContext must be used within LyricsProvider');
43
+ return ctx;
44
+ }
@@ -0,0 +1,24 @@
1
+ import type { ReactNode } from 'react';
2
+ export type PanelMode = 'none' | 'lyrics' | 'queue' | 'search';
3
+ type PanelModeContextValue = {
4
+ panelMode: PanelMode;
5
+ hasPanel: boolean;
6
+ isInputMode: boolean;
7
+ setInputMode: (v: boolean) => void;
8
+ toggleLyrics: () => void;
9
+ toggleQueue: () => void;
10
+ toggleSearch: () => void;
11
+ };
12
+ /**
13
+ * Provides shared panel mode state. Only one panel can be active at a time.
14
+ * Toggling the active panel closes it; toggling a different panel switches to it.
15
+ * Tracks whether a text input is active (isInputMode) to suppress keyboard shortcuts.
16
+ */
17
+ export declare function PanelModeProvider({ children }: {
18
+ children: ReactNode;
19
+ }): import("react/jsx-runtime").JSX.Element;
20
+ /**
21
+ * Access the current panel mode and toggle actions.
22
+ */
23
+ export declare function usePanelMode(): PanelModeContextValue;
24
+ export {};
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useContext, useState } from 'react';
3
+ const PanelModeContext = createContext(null);
4
+ /**
5
+ * Provides shared panel mode state. Only one panel can be active at a time.
6
+ * Toggling the active panel closes it; toggling a different panel switches to it.
7
+ * Tracks whether a text input is active (isInputMode) to suppress keyboard shortcuts.
8
+ */
9
+ export function PanelModeProvider({ children }) {
10
+ const [panelMode, setPanelMode] = useState('none');
11
+ const [isInputMode, setInputMode] = useState(false);
12
+ const toggle = useCallback((mode) => {
13
+ setPanelMode((v) => {
14
+ const next = v === mode ? 'none' : mode;
15
+ if (next !== 'search')
16
+ setInputMode(false);
17
+ return next;
18
+ });
19
+ }, []);
20
+ const value = {
21
+ panelMode,
22
+ hasPanel: panelMode !== 'none',
23
+ isInputMode,
24
+ setInputMode,
25
+ toggleLyrics: useCallback(() => toggle('lyrics'), [
26
+ toggle,
27
+ ]),
28
+ toggleQueue: useCallback(() => toggle('queue'), [
29
+ toggle,
30
+ ]),
31
+ toggleSearch: useCallback(() => toggle('search'), [
32
+ toggle,
33
+ ]),
34
+ };
35
+ return _jsx(PanelModeContext, { value: value, children: children });
36
+ }
37
+ /**
38
+ * Access the current panel mode and toggle actions.
39
+ */
40
+ export function usePanelMode() {
41
+ const ctx = useContext(PanelModeContext);
42
+ if (!ctx)
43
+ throw new Error('usePanelMode must be used within PanelModeProvider');
44
+ return ctx;
45
+ }
@@ -0,0 +1,32 @@
1
+ import type { SpotifyApi } from '@spotify/web-api-ts-sdk';
2
+ import type { ReactNode } from 'react';
3
+ import type { QueueItem } from '../hooks/use-queue.js';
4
+ import type { TrackInfo } from '../spotify/playback.js';
5
+ type QueueContextValue = {
6
+ queue: QueueItem[];
7
+ contextName: string | null;
8
+ contextSubtitle: string | null;
9
+ isLoading: boolean;
10
+ selectedIndex: number;
11
+ refresh: () => Promise<void>;
12
+ queueUp: (() => void) | undefined;
13
+ queueDown: (() => void) | undefined;
14
+ queueSelect: (() => void) | undefined;
15
+ };
16
+ type QueueProviderProps = {
17
+ client: SpotifyApi;
18
+ track: TrackInfo | null;
19
+ playTrackUri: (uri: string) => Promise<void>;
20
+ refresh: () => Promise<void>;
21
+ children: ReactNode;
22
+ };
23
+ /**
24
+ * Provides queue data and navigation controls.
25
+ * Only fetches queue when the queue panel is active.
26
+ */
27
+ export declare function QueueProvider({ client, track, playTrackUri, refresh, children }: QueueProviderProps): import("react/jsx-runtime").JSX.Element;
28
+ /**
29
+ * Access queue data and navigation controls.
30
+ */
31
+ export declare function useQueueContext(): QueueContextValue;
32
+ export {};
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useContext, useState } from 'react';
3
+ import { useQueue } from '../hooks/use-queue.js';
4
+ import { playInContext } from '../spotify/playback.js';
5
+ import { usePanelMode } from './panel-mode-context.js';
6
+ const QueueContext = createContext(null);
7
+ /**
8
+ * Provides queue data and navigation controls.
9
+ * Only fetches queue when the queue panel is active.
10
+ */
11
+ export function QueueProvider({ client, track, playTrackUri, refresh, children }) {
12
+ const { panelMode } = usePanelMode();
13
+ const isActive = panelMode === 'queue';
14
+ const { queue, contextName, contextSubtitle, isLoading, currentIndex, refresh: refreshQueue, } = useQueue(client, isActive, track?.context ?? null, track?.uri ?? null);
15
+ const [selectedIndex, setSelectedIndex] = useState(0);
16
+ const handleSelect = useCallback(async () => {
17
+ const item = queue[selectedIndex];
18
+ const deviceId = track?.deviceId;
19
+ const context = track?.context;
20
+ if (!item || !deviceId)
21
+ return;
22
+ if (context) {
23
+ await playInContext(client, deviceId, context.uri, item.uri);
24
+ }
25
+ else {
26
+ await playTrackUri(item.uri);
27
+ }
28
+ await refresh();
29
+ }, [
30
+ client,
31
+ queue,
32
+ selectedIndex,
33
+ track,
34
+ playTrackUri,
35
+ refresh,
36
+ ]);
37
+ // Reset selection to current track when opening the queue
38
+ const prevIsActive = useCallback(() => {
39
+ setSelectedIndex(currentIndex);
40
+ }, [
41
+ currentIndex,
42
+ ]);
43
+ // Call reset when becoming active
44
+ if (isActive && selectedIndex === 0 && currentIndex > 0) {
45
+ prevIsActive();
46
+ }
47
+ const value = {
48
+ queue,
49
+ contextName,
50
+ contextSubtitle,
51
+ isLoading,
52
+ selectedIndex,
53
+ refresh: refreshQueue,
54
+ queueUp: isActive ? () => setSelectedIndex((v) => Math.max(0, v - 1)) : undefined,
55
+ queueDown: isActive ? () => setSelectedIndex((v) => Math.min(Math.max(queue.length - 1, 0), v + 1)) : undefined,
56
+ queueSelect: isActive ? handleSelect : undefined,
57
+ };
58
+ return _jsx(QueueContext, { value: value, children: children });
59
+ }
60
+ /**
61
+ * Access queue data and navigation controls.
62
+ */
63
+ export function useQueueContext() {
64
+ const ctx = useContext(QueueContext);
65
+ if (!ctx)
66
+ throw new Error('useQueueContext must be used within QueueProvider');
67
+ return ctx;
68
+ }
@@ -0,0 +1,59 @@
1
+ import type { SpotifyApi } from '@spotify/web-api-ts-sdk';
2
+ import type { ReactNode } from 'react';
3
+ import type { BrowseData } from '../hooks/use-browse.js';
4
+ import type { TrackInfo } from '../spotify/playback.js';
5
+ import type { SearchResultPlaylist, SearchResults } from '../spotify/search.js';
6
+ export type SearchView = {
7
+ kind: 'input';
8
+ } | {
9
+ kind: 'album';
10
+ id: string;
11
+ name: string;
12
+ } | {
13
+ kind: 'playlist';
14
+ id: string;
15
+ name: string;
16
+ } | {
17
+ kind: 'playlists';
18
+ };
19
+ export type SearchCategory = 'tracks' | 'albums' | 'artists' | 'playlists';
20
+ type SearchContextValue = {
21
+ query: string;
22
+ results: SearchResults | null;
23
+ isSearchLoading: boolean;
24
+ currentView: SearchView;
25
+ canGoBack: boolean;
26
+ browseData: BrowseData | null;
27
+ isBrowseLoading: boolean;
28
+ userPlaylists: SearchResultPlaylist[] | null;
29
+ isPlaylistsLoading: boolean;
30
+ selectedCategory: SearchCategory;
31
+ selectedIndex: number;
32
+ currentTrackUri: string | null;
33
+ currentContextUri: string | null;
34
+ setQuery: ((q: string) => void) | undefined;
35
+ navigateUp: (() => void) | undefined;
36
+ navigateDown: (() => void) | undefined;
37
+ navigateLeft: (() => void) | undefined;
38
+ navigateRight: (() => void) | undefined;
39
+ select: (() => void) | undefined;
40
+ goBack: (() => void) | undefined;
41
+ browseUserPlaylists: (() => void) | undefined;
42
+ };
43
+ type SearchProviderProps = {
44
+ client: SpotifyApi;
45
+ track: TrackInfo | null;
46
+ playTrackUri: (uri: string) => Promise<void>;
47
+ refresh: () => Promise<void>;
48
+ children: ReactNode;
49
+ };
50
+ /**
51
+ * Provides search state, navigation stack, and selection controls.
52
+ * Only active when the search panel is open.
53
+ */
54
+ export declare function SearchProvider({ client, track, playTrackUri, refresh, children }: SearchProviderProps): import("react/jsx-runtime").JSX.Element;
55
+ /**
56
+ * Access search state and navigation controls.
57
+ */
58
+ export declare function useSearchContext(): SearchContextValue;
59
+ export {};