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
@@ -0,0 +1,215 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Spacer, Text, useWindowSize } from 'ink';
3
+ import { useMemo } from 'react';
4
+ import { LYRICS_WIDE_COLUMNS } from '../config.js';
5
+ import { LyricsProvider, useLyricsContext } from '../contexts/lyrics-context.js';
6
+ import { PanelModeProvider, usePanelMode } from '../contexts/panel-mode-context.js';
7
+ import { QueueProvider } from '../contexts/queue-context.js';
8
+ import { SearchProvider } from '../contexts/search-context.js';
9
+ import { useAlbumArt } from '../hooks/use-album-art.js';
10
+ import { usePlayback } from '../hooks/use-playback.js';
11
+ import { usePlayerInput } from '../hooks/use-player-input.js';
12
+ import { KeyboardHints } from './keyboard-hints.js';
13
+ import { PanelContent } from './panel-content.js';
14
+ import { PlaybackStatus } from './playback-status.js';
15
+ import { SidePanel } from './side-panel.js';
16
+ const MIN_WIDE_COLUMNS = 60;
17
+ const ART_WIDTH = 20;
18
+ const TIME_LABEL_WIDTH = 15;
19
+ const HEADER_FOOTER_ROWS = 8;
20
+ const MAX_ART_HEIGHT = 12;
21
+ const MIN_PANEL_HEIGHT = 5;
22
+ const PANEL_RESERVED_ROWS = 6;
23
+ function computeLayout(columns, rows, hasPanel, hasArt) {
24
+ const padding = 2;
25
+ const isWide = columns >= MIN_WIDE_COLUMNS;
26
+ const isPanelWide = columns >= LYRICS_WIDE_COLUMNS;
27
+ const artSpace = hasArt ? ART_WIDTH + 2 : 0;
28
+ const statusWidth = 5;
29
+ const panelColumnWidth = isPanelWide && hasPanel ? Math.floor((columns - padding * 2) / 2) : 0;
30
+ const leftWidth = columns - padding * 2 - panelColumnWidth - (panelColumnWidth > 0 ? 3 : 0);
31
+ return {
32
+ isWide,
33
+ isPanelWide,
34
+ artHeight: isWide && !hasPanel ? Math.min(rows - HEADER_FOOTER_ROWS, MAX_ART_HEIGHT) : 0,
35
+ progressBarWidth: Math.max(leftWidth - artSpace - statusWidth - TIME_LABEL_WIDTH, 10),
36
+ panelColumnWidth,
37
+ leftWidth,
38
+ panelHeight: Math.max(rows - PANEL_RESERVED_ROWS, MIN_PANEL_HEIGHT),
39
+ };
40
+ }
41
+ function buildHints(panelMode, lyricsOffset, isPlainText, isInputMode) {
42
+ if (panelMode === 'search' && isInputMode) {
43
+ return [
44
+ {
45
+ key: 'type',
46
+ label: 'search',
47
+ },
48
+ {
49
+ key: 'enter',
50
+ label: 'submit',
51
+ },
52
+ {
53
+ key: 'esc',
54
+ label: 'close',
55
+ },
56
+ ];
57
+ }
58
+ if (panelMode === 'search') {
59
+ return [
60
+ {
61
+ key: '\u2191/\u2193',
62
+ label: 'navigate',
63
+ },
64
+ {
65
+ key: '\u2190/\u2192',
66
+ label: 'category',
67
+ },
68
+ {
69
+ key: 'enter',
70
+ label: 'select',
71
+ },
72
+ {
73
+ key: 'esc',
74
+ label: 'back',
75
+ },
76
+ {
77
+ key: 'q',
78
+ label: 'quit',
79
+ },
80
+ ];
81
+ }
82
+ if (panelMode === 'queue') {
83
+ return [
84
+ {
85
+ key: '\u2191/\u2193',
86
+ label: 'navigate',
87
+ },
88
+ {
89
+ key: 'enter',
90
+ label: 'play',
91
+ },
92
+ {
93
+ key: 'd',
94
+ label: 'close',
95
+ },
96
+ {
97
+ key: 'esc',
98
+ label: 'close',
99
+ },
100
+ {
101
+ key: 'q',
102
+ label: 'quit',
103
+ },
104
+ ];
105
+ }
106
+ if (panelMode === 'lyrics') {
107
+ const hints = [];
108
+ if (isPlainText) {
109
+ hints.push({
110
+ key: '\u2191/\u2193',
111
+ label: 'scroll',
112
+ });
113
+ }
114
+ hints.push({
115
+ key: '+/-',
116
+ label: `offset(${lyricsOffset}ms)`,
117
+ }, {
118
+ key: 'l',
119
+ label: 'close',
120
+ }, {
121
+ key: 'esc',
122
+ label: 'close',
123
+ }, {
124
+ key: 'q',
125
+ label: 'quit',
126
+ });
127
+ return hints;
128
+ }
129
+ return [
130
+ {
131
+ key: 'space',
132
+ label: 'play/pause',
133
+ },
134
+ {
135
+ key: 'n',
136
+ label: 'next',
137
+ },
138
+ {
139
+ key: 'p',
140
+ label: 'prev',
141
+ },
142
+ {
143
+ key: '\u2190/\u2192',
144
+ label: 'seek',
145
+ },
146
+ {
147
+ key: '\u2191/\u2193',
148
+ label: 'vol',
149
+ },
150
+ {
151
+ key: 's',
152
+ label: 'shuffle',
153
+ },
154
+ {
155
+ key: 'r',
156
+ label: 'repeat',
157
+ },
158
+ {
159
+ key: 'l',
160
+ label: 'lyrics',
161
+ },
162
+ {
163
+ key: 'd',
164
+ label: 'queue',
165
+ },
166
+ {
167
+ key: '/',
168
+ label: 'search',
169
+ },
170
+ {
171
+ key: 'b',
172
+ label: 'playlists',
173
+ },
174
+ {
175
+ key: 'q',
176
+ label: 'quit',
177
+ },
178
+ ];
179
+ }
180
+ /**
181
+ * Player component wrapped with context providers.
182
+ */
183
+ export function Player({ client }) {
184
+ return (_jsx(PanelModeProvider, { children: _jsx(PlayerWithProviders, { client: client }) }));
185
+ }
186
+ /**
187
+ * Sets up playback-dependent providers, then renders the UI.
188
+ */
189
+ function PlayerWithProviders({ client }) {
190
+ const playback = usePlayback(client);
191
+ const { track, isLoading, error } = playback;
192
+ const { columns, rows } = useWindowSize();
193
+ const { panelMode, hasPanel } = usePanelMode();
194
+ return (_jsx(LyricsProvider, { track: track, children: _jsx(QueueProvider, { client: client, track: track, playTrackUri: playback.playTrackUri, refresh: playback.refresh, children: _jsx(SearchProvider, { client: client, track: track, playTrackUri: playback.playTrackUri, refresh: playback.refresh, children: _jsx(PlayerUI, { playback: playback, track: track, isLoading: isLoading, error: error, columns: columns, rows: rows, panelMode: panelMode, hasPanel: hasPanel }) }) }) }));
195
+ }
196
+ /**
197
+ * Pure UI component - consumes contexts for panel data, receives playback as props.
198
+ */
199
+ function PlayerUI({ playback, track, isLoading, error, columns, rows, panelMode, hasPanel }) {
200
+ const { offset: lyricsOffset, isPlainText: isPlainLyrics } = useLyricsContext();
201
+ const { isInputMode } = usePanelMode();
202
+ const layout = computeLayout(columns, rows, hasPanel, false);
203
+ const art = useAlbumArt(layout.artHeight > 0 ? (track?.albumImageUrl ?? null) : null, layout.artHeight);
204
+ const finalLayout = art ? computeLayout(columns, rows, hasPanel, true) : layout;
205
+ usePlayerInput(playback);
206
+ const hints = useMemo(() => buildHints(panelMode, lyricsOffset, isPlainLyrics, isInputMode), [
207
+ panelMode,
208
+ lyricsOffset,
209
+ isPlainLyrics,
210
+ isInputMode,
211
+ ]);
212
+ const showPanelWide = finalLayout.isPanelWide && hasPanel && !!track;
213
+ const showPanelNarrow = !finalLayout.isPanelWide && hasPanel && !!track;
214
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: "100%", borderColor: "black", borderStyle: "single", children: [_jsx(Text, { bold: true, color: "green", children: "morille" }), _jsxs(Box, { marginTop: 1, flexDirection: showPanelWide ? 'row' : 'column', flexGrow: 1, children: [_jsx(Box, { flexDirection: "column", width: showPanelWide ? finalLayout.leftWidth : undefined, flexGrow: showPanelWide ? 0 : 1, children: _jsx(PlaybackStatus, { track: track, isLoading: isLoading, error: error, art: art, progressBarWidth: finalLayout.progressBarWidth }) }), showPanelWide && track && (_jsx(SidePanel, { width: finalLayout.panelColumnWidth || undefined, isWideLayout: true, children: _jsx(PanelContent, { progressMs: track.progressMs, height: finalLayout.panelHeight }) }))] }), showPanelNarrow && track && (_jsx(SidePanel, { width: undefined, isWideLayout: false, children: _jsx(PanelContent, { progressMs: track.progressMs, height: finalLayout.panelHeight }) })), _jsx(Spacer, {}), _jsx(KeyboardHints, { hints: hints })] }));
215
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"player.js","sourceRoot":"","sources":["../../src/components/player.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAChC,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEjF,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAM5C,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAC5B,MAAM,SAAS,GAAG,EAAE,CAAC;AACrB,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAC5B,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAC7B,MAAM,cAAc,GAAG,EAAE,CAAC;AAC1B,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAC3B,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAE9B,SAAS,aAAa,CAAC,OAAe,EAAE,IAAY,EAAE,QAAiB,EAAE,MAAe;IACtF,MAAM,OAAO,GAAG,CAAC,CAAC;IAClB,MAAM,MAAM,GAAG,OAAO,IAAI,gBAAgB,CAAC;IAC3C,MAAM,WAAW,GAAG,OAAO,IAAI,mBAAmB,CAAC;IACnD,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,MAAM,WAAW,GAAG,CAAC,CAAC;IACtB,MAAM,gBAAgB,GAAG,WAAW,IAAI,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,OAAO,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/F,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,CAAC,GAAG,gBAAgB,GAAG,CAAC,gBAAgB,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAE5F,OAAO;QACL,MAAM;QACN,WAAW;QACX,SAAS,EAAE,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,kBAAkB,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC;QACxF,gBAAgB,EAAE,IAAI,CAAC,GAAG,CAAC,SAAS,GAAG,QAAQ,GAAG,WAAW,GAAG,gBAAgB,EAAE,EAAE,CAAC;QACrF,gBAAgB;QAChB,SAAS;QACT,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,mBAAmB,EAAE,gBAAgB,CAAC;KACpE,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,SAAoB,EAAE,YAAoB,EAAE,WAAoB,EAAE,WAAoB;IACxG,IAAI,SAAS,KAAK,QAAQ,IAAI,WAAW,EAAE,CAAC;QAC1C,OAAO;YACL;gBACE,GAAG,EAAE,MAAM;gBACX,KAAK,EAAE,QAAQ;aAChB;YACD;gBACE,GAAG,EAAE,OAAO;gBACZ,KAAK,EAAE,QAAQ;aAChB;YACD;gBACE,GAAG,EAAE,KAAK;gBACV,KAAK,EAAE,OAAO;aACf;SACF,CAAC;IACJ,CAAC;IAED,IAAI,SAAS,KAAK,QAAQ,EAAE,CAAC;QAC3B,OAAO;YACL;gBACE,GAAG,EAAE,eAAe;gBACpB,KAAK,EAAE,UAAU;aAClB;YACD;gBACE,GAAG,EAAE,eAAe;gBACpB,KAAK,EAAE,UAAU;aAClB;YACD;gBACE,GAAG,EAAE,OAAO;gBACZ,KAAK,EAAE,QAAQ;aAChB;YACD;gBACE,GAAG,EAAE,KAAK;gBACV,KAAK,EAAE,MAAM;aACd;YACD;gBACE,GAAG,EAAE,GAAG;gBACR,KAAK,EAAE,MAAM;aACd;SACF,CAAC;IACJ,CAAC;IAED,IAAI,SAAS,KAAK,OAAO,EAAE,CAAC;QAC1B,OAAO;YACL;gBACE,GAAG,EAAE,eAAe;gBACpB,KAAK,EAAE,UAAU;aAClB;YACD;gBACE,GAAG,EAAE,OAAO;gBACZ,KAAK,EAAE,MAAM;aACd;YACD;gBACE,GAAG,EAAE,GAAG;gBACR,KAAK,EAAE,OAAO;aACf;YACD;gBACE,GAAG,EAAE,KAAK;gBACV,KAAK,EAAE,OAAO;aACf;YACD;gBACE,GAAG,EAAE,GAAG;gBACR,KAAK,EAAE,MAAM;aACd;SACF,CAAC;IACJ,CAAC;IAED,IAAI,SAAS,KAAK,QAAQ,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,EAAE,CAAC;QACjB,IAAI,WAAW,EAAE,CAAC;YAChB,KAAK,CAAC,IAAI,CAAC;gBACT,GAAG,EAAE,eAAe;gBACpB,KAAK,EAAE,QAAQ;aAChB,CAAC,CAAC;QACL,CAAC;QACD,KAAK,CAAC,IAAI,CACR;YACE,GAAG,EAAE,KAAK;YACV,KAAK,EAAE,UAAU,YAAY,KAAK;SACnC,EACD;YACE,GAAG,EAAE,GAAG;YACR,KAAK,EAAE,OAAO;SACf,EACD;YACE,GAAG,EAAE,KAAK;YACV,KAAK,EAAE,OAAO;SACf,EACD;YACE,GAAG,EAAE,GAAG;YACR,KAAK,EAAE,MAAM;SACd,CACF,CAAC;QACF,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO;QACL;YACE,GAAG,EAAE,OAAO;YACZ,KAAK,EAAE,YAAY;SACpB;QACD;YACE,GAAG,EAAE,GAAG;YACR,KAAK,EAAE,MAAM;SACd;QACD;YACE,GAAG,EAAE,GAAG;YACR,KAAK,EAAE,MAAM;SACd;QACD;YACE,GAAG,EAAE,eAAe;YACpB,KAAK,EAAE,MAAM;SACd;QACD;YACE,GAAG,EAAE,eAAe;YACpB,KAAK,EAAE,KAAK;SACb;QACD;YACE,GAAG,EAAE,GAAG;YACR,KAAK,EAAE,SAAS;SACjB;QACD;YACE,GAAG,EAAE,GAAG;YACR,KAAK,EAAE,QAAQ;SAChB;QACD;YACE,GAAG,EAAE,GAAG;YACR,KAAK,EAAE,QAAQ;SAChB;QACD;YACE,GAAG,EAAE,GAAG;YACR,KAAK,EAAE,OAAO;SACf;QACD;YACE,GAAG,EAAE,GAAG;YACR,KAAK,EAAE,QAAQ;SAChB;QACD;YACE,GAAG,EAAE,GAAG;YACR,KAAK,EAAE,WAAW;SACnB;QACD;YACE,GAAG,EAAE,GAAG;YACR,KAAK,EAAE,MAAM;SACd;KACF,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,MAAM,CAAC,EAAE,MAAM,EAAe;IAC5C,OAAO,CACL,KAAC,iBAAiB,cAChB,KAAC,mBAAmB,IAAC,MAAM,EAAE,MAAM,GAAI,GACrB,CACrB,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,SAAS,mBAAmB,CAAC,EAAE,MAAM,EAAe;IAClD,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,QAAQ,CAAC;IAC7C,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,aAAa,EAAE,CAAC;IAC1C,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,YAAY,EAAE,CAAC;IAE/C,OAAO,CACL,KAAC,cAAc,IAAC,KAAK,EAAE,KAAK,YAC1B,KAAC,aAAa,IAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,QAAQ,CAAC,YAAY,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,YACzG,KAAC,cAAc,IAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,QAAQ,CAAC,YAAY,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,YAC1G,KAAC,QAAQ,IACP,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,IAAI,EACV,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,QAAQ,GAClB,GACa,GACH,GACD,CAClB,CAAC;AACJ,CAAC;AAaD;;GAEG;AACH,SAAS,QAAQ,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAiB;IACxG,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,GAAG,gBAAgB,EAAE,CAAC;IAChF,MAAM,EAAE,WAAW,EAAE,GAAG,YAAY,EAAE,CAAC;IAEvC,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC7D,MAAM,GAAG,GAAG,WAAW,CAAC,MAAM,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,aAAa,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;IACxG,MAAM,WAAW,GAAG,GAAG,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAEhF,cAAc,CAAC,QAAQ,CAAC,CAAC;IAEzB,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,UAAU,CAAC,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,CAAC,EACrE;QACE,SAAS;QACT,YAAY;QACZ,aAAa;QACb,WAAW;KACZ,CACF,CAAC;IAEF,MAAM,aAAa,GAAG,WAAW,CAAC,WAAW,IAAI,QAAQ,IAAI,CAAC,CAAC,KAAK,CAAC;IACrE,MAAM,eAAe,GAAG,CAAC,WAAW,CAAC,WAAW,IAAI,QAAQ,IAAI,CAAC,CAAC,KAAK,CAAC;IAExE,OAAO,CACL,MAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,EAAC,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAC,MAAM,EAAC,WAAW,EAAC,OAAO,EAAC,WAAW,EAAC,QAAQ,aAC7F,KAAC,IAAI,IAAC,IAAI,QAAC,KAAK,EAAC,OAAO,wBAEjB,EAEP,MAAC,GAAG,IAAC,SAAS,EAAE,CAAC,EAAE,aAAa,EAAE,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,CAAC,aAC7E,KAAC,GAAG,IACF,aAAa,EAAC,QAAQ,EACtB,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EACxD,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,YAE/B,KAAC,cAAc,IACb,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,GAAG,EACR,gBAAgB,EAAE,WAAW,CAAC,gBAAgB,GAC9C,GACE,EAEL,aAAa,IAAI,KAAK,IAAI,CACzB,KAAC,SAAS,IAAC,KAAK,EAAE,WAAW,CAAC,gBAAgB,IAAI,SAAS,EAAE,YAAY,kBACvE,KAAC,YAAY,IAAC,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,WAAW,GAAI,GACrE,CACb,IACG,EAEL,eAAe,IAAI,KAAK,IAAI,CAC3B,KAAC,SAAS,IAAC,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,YAC9C,KAAC,YAAY,IAAC,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,WAAW,GAAI,GACrE,CACb,EAED,KAAC,MAAM,KAAG,EAEV,KAAC,aAAa,IAAC,KAAK,EAAE,KAAK,GAAI,IAC3B,CACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,11 @@
1
+ type ProgressBarProps = {
2
+ progress: number;
3
+ duration: number;
4
+ width: number;
5
+ };
6
+ /**
7
+ * Renders a visual progress bar using Unicode block characters.
8
+ * Width adapts to the available terminal space.
9
+ */
10
+ export declare function ProgressBar({ progress, duration, width }: ProgressBarProps): import("react/jsx-runtime").JSX.Element;
11
+ export {};
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ /**
4
+ * Renders a visual progress bar using Unicode block characters.
5
+ * Width adapts to the available terminal space.
6
+ */
7
+ export function ProgressBar({ progress, duration, width }) {
8
+ const barWidth = Math.max(width, 5);
9
+ const ratio = duration > 0 ? Math.min(progress / duration, 1) : 0;
10
+ const filled = Math.round(ratio * barWidth);
11
+ const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
12
+ return _jsx(Text, { dimColor: true, children: bar });
13
+ }
@@ -0,0 +1,9 @@
1
+ type QueueViewProps = {
2
+ height: number;
3
+ };
4
+ /**
5
+ * Queue view reading data from QueueContext.
6
+ * Split into history (dimmed) and upcoming sections with the current track between them.
7
+ */
8
+ export declare function QueueView({ height }: QueueViewProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useQueueContext } from '../contexts/queue-context.js';
4
+ function QueueSlot({ item, isSelected }) {
5
+ const label = `${item.artist} - ${item.name}`;
6
+ if (item.isCurrent) {
7
+ return (_jsxs(Text, { bold: true, color: "green", inverse: isSelected, children: ['\u25B6', " ", label] }));
8
+ }
9
+ if (item.isPrevious) {
10
+ return (_jsx(Text, { dimColor: true, inverse: isSelected, children: label }));
11
+ }
12
+ if (isSelected) {
13
+ return _jsx(Text, { inverse: true, children: label });
14
+ }
15
+ return _jsx(Text, { children: label });
16
+ }
17
+ function renderList(items, selectedIndex, offset, height) {
18
+ if (items.length === 0)
19
+ return null;
20
+ const halfHeight = Math.floor(height / 2);
21
+ const startIndex = Math.max(0, Math.min(selectedIndex - offset - halfHeight, items.length - height));
22
+ const visibleCount = Math.min(height, items.length);
23
+ return Array.from({
24
+ length: visibleCount,
25
+ }, (_, i) => {
26
+ const idx = startIndex + i;
27
+ const item = items[idx];
28
+ if (!item)
29
+ return _jsx(Text, { children: " " }, idx);
30
+ return _jsx(QueueSlot, { item: item, isSelected: idx + offset === selectedIndex }, item.uri + idx);
31
+ });
32
+ }
33
+ /**
34
+ * Queue view reading data from QueueContext.
35
+ * Split into history (dimmed) and upcoming sections with the current track between them.
36
+ */
37
+ export function QueueView({ height }) {
38
+ const { queue, selectedIndex, isLoading, contextName, contextSubtitle } = useQueueContext();
39
+ if (isLoading) {
40
+ return _jsx(Text, { dimColor: true, children: "Loading queue..." });
41
+ }
42
+ if (queue.length === 0) {
43
+ return _jsx(Text, { dimColor: true, children: "Queue is empty" });
44
+ }
45
+ const history = queue.filter((q) => q.isPrevious);
46
+ const current = queue.find((q) => q.isCurrent);
47
+ const currentIndex = queue.findIndex((q) => q.isCurrent);
48
+ const upcoming = queue.filter((q) => !q.isCurrent && !q.isPrevious);
49
+ const headerLines = 1 + (current ? 1 : 0);
50
+ const availableHeight = Math.max(height - headerLines, 2);
51
+ const historyHeight = Math.min(Math.floor(availableHeight / 2), history.length);
52
+ const upcomingHeight = Math.min(availableHeight - historyHeight, upcoming.length);
53
+ return (_jsxs(Box, { flexDirection: "column", height: height, overflow: "hidden", children: [contextName && (_jsxs(Box, { gap: 1, borderColor: "gray", borderStyle: "single", borderBottom: true, borderLeft: false, borderRight: false, borderTop: false, children: [_jsx(Text, { bold: true, children: contextName }), contextSubtitle && _jsx(Text, { children: "-" }), contextSubtitle && _jsx(Text, { dimColor: true, children: contextSubtitle })] })), renderList(history, selectedIndex, 0, historyHeight), current && _jsx(QueueSlot, { item: current, isSelected: currentIndex === selectedIndex }), renderList(upcoming, selectedIndex, history.length + (current ? 1 : 0), upcomingHeight)] }));
54
+ }
@@ -0,0 +1,8 @@
1
+ type SearchPanelProps = {
2
+ height: number;
3
+ };
4
+ /**
5
+ * Routes between search sub-views based on the current navigation state.
6
+ */
7
+ export declare function SearchPanel({ height }: SearchPanelProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,152 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { usePanelMode } from '../contexts/panel-mode-context.js';
4
+ import { useSearchContext } from '../contexts/search-context.js';
5
+ import { BrowseDetailView } from './browse-detail-view.js';
6
+ /**
7
+ * Routes between search sub-views based on the current navigation state.
8
+ */
9
+ export function SearchPanel({ height }) {
10
+ const ctx = useSearchContext();
11
+ switch (ctx.currentView.kind) {
12
+ case 'input':
13
+ return _jsx(SearchInputView, { height: height });
14
+ case 'album':
15
+ case 'playlist':
16
+ return _jsx(SearchBrowseView, { height: height });
17
+ case 'playlists':
18
+ return _jsx(UserPlaylistsView, { height: height });
19
+ }
20
+ }
21
+ function SearchInputView({ height }) {
22
+ const { query, results, isSearchLoading, selectedCategory, selectedIndex, currentTrackUri, currentContextUri } = useSearchContext();
23
+ const { isInputMode } = usePanelMode();
24
+ // Header = 2 content lines (input + hint) + 1 border line below
25
+ const headerLines = 3;
26
+ const availableHeight = Math.max(height - headerLines, 2);
27
+ return (_jsxs(Box, { flexDirection: "column", height: height, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", borderColor: "gray", borderStyle: "single", borderBottom: true, borderLeft: false, borderRight: false, borderTop: false, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, dimColor: !isInputMode, children: "Search:" }), _jsx(Text, { dimColor: !isInputMode, children: query.length > 0 ? query : _jsx(Text, { dimColor: true, children: "type to search..." }) }), isInputMode && _jsx(Text, { color: "green", children: '_' })] }), _jsx(Box, { children: results &&
28
+ (isInputMode ? (_jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, children: "enter" }), "/", _jsx(Text, { bold: true, children: "tab" }), " browse results"] })) : (_jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, children: "esc" }), " back to input"] }))) })] }), isSearchLoading && !results && _jsx(Text, { dimColor: true, children: "Searching..." }), results && (_jsx(ResultsView, { results: results, selectedCategory: selectedCategory, selectedIndex: selectedIndex, currentTrackUri: currentTrackUri, currentContextUri: currentContextUri, height: availableHeight, dimmed: isInputMode })), !results && !isSearchLoading && query.length === 0 && _jsx(Text, { dimColor: true, children: "Type a query to search Spotify" })] }));
29
+ }
30
+ function ResultsView({ results, selectedCategory, selectedIndex, currentTrackUri, currentContextUri, height, dimmed, }) {
31
+ const categories = [
32
+ {
33
+ key: 'tracks',
34
+ label: 'Tracks',
35
+ },
36
+ {
37
+ key: 'albums',
38
+ label: 'Albums',
39
+ },
40
+ {
41
+ key: 'artists',
42
+ label: 'Artists',
43
+ },
44
+ {
45
+ key: 'playlists',
46
+ label: 'Playlists',
47
+ },
48
+ ];
49
+ const items = results[selectedCategory];
50
+ // 1 line for tabs + 1 line for border below
51
+ const listHeight = Math.max(height - 2, 1);
52
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { gap: 1, borderColor: "gray", borderStyle: "single", borderBottom: true, borderLeft: false, borderRight: false, borderTop: false, children: categories.map((cat) => {
53
+ const count = results[cat.key].length;
54
+ const isActive = cat.key === selectedCategory;
55
+ return (_jsxs(Text, { bold: isActive && !dimmed, underline: isActive, dimColor: dimmed || !isActive || count === 0, children: [cat.label, "(", count, ")"] }, cat.key));
56
+ }) }), _jsxs(Box, { flexDirection: "column", height: listHeight, overflow: "hidden", children: [items.length === 0 && _jsx(Text, { dimColor: true, children: "No results" }), items.map((item, idx) => (_jsx(ResultItem, { category: selectedCategory, item: item, isSelected: idx === selectedIndex, isPlaying: isItemPlaying(selectedCategory, item, currentTrackUri, currentContextUri), dimmed: dimmed }, itemKey(selectedCategory, item))))] })] }));
57
+ }
58
+ /**
59
+ * Returns true if a search result item matches the currently playing track or playback context.
60
+ * - Tracks match by URI.
61
+ * - Albums / playlists match when they are the current playback context.
62
+ * - Artists never match (Spotify's playback context is never an artist URI).
63
+ */
64
+ function isItemPlaying(category, item, currentTrackUri, currentContextUri) {
65
+ switch (category) {
66
+ case 'tracks':
67
+ return currentTrackUri !== null && item.uri === currentTrackUri;
68
+ case 'albums':
69
+ return currentContextUri !== null && item.uri === currentContextUri;
70
+ case 'playlists':
71
+ return currentContextUri !== null && item.uri === currentContextUri;
72
+ case 'artists':
73
+ return false;
74
+ }
75
+ }
76
+ function ResultItem({ category, item, isSelected, isPlaying, dimmed, }) {
77
+ const label = formatResultItem(category, item);
78
+ // No selection indicator while dimmed (input focused) — selection only matters in browse mode
79
+ const icon = isPlaying ? '\u25B6 ' : !dimmed && isSelected ? '> ' : ' ';
80
+ if (isPlaying) {
81
+ return (_jsxs(Text, { bold: true, color: "green", inverse: !dimmed && isSelected, dimColor: dimmed, children: [icon, label] }));
82
+ }
83
+ return (_jsxs(Text, { inverse: !dimmed && isSelected, dimColor: dimmed, children: [icon, label] }));
84
+ }
85
+ function formatResultItem(category, item) {
86
+ switch (category) {
87
+ case 'tracks': {
88
+ const t = item;
89
+ return `${t.artist} - ${t.name}`;
90
+ }
91
+ case 'albums': {
92
+ const a = item;
93
+ return `${a.name} - ${a.artist} (${a.totalTracks} tracks)`;
94
+ }
95
+ case 'artists': {
96
+ const ar = item;
97
+ return ar.name;
98
+ }
99
+ case 'playlists': {
100
+ const p = item;
101
+ return `${p.name} (${p.owner}, ${p.totalTracks} tracks)`;
102
+ }
103
+ }
104
+ }
105
+ function itemKey(category, item) {
106
+ switch (category) {
107
+ case 'tracks':
108
+ return item.uri;
109
+ case 'albums':
110
+ return item.id;
111
+ case 'artists':
112
+ return item.id;
113
+ case 'playlists':
114
+ return item.id;
115
+ }
116
+ }
117
+ function SearchBrowseView({ height }) {
118
+ const { browseData, isBrowseLoading, selectedIndex, currentTrackUri } = useSearchContext();
119
+ if (isBrowseLoading || !browseData) {
120
+ return _jsx(Text, { dimColor: true, children: "Loading..." });
121
+ }
122
+ return (_jsx(BrowseDetailView, { data: browseData, selectedIndex: selectedIndex, currentTrackUri: currentTrackUri, height: height }));
123
+ }
124
+ function UserPlaylistsView({ height }) {
125
+ const { userPlaylists, isPlaylistsLoading, selectedIndex, currentContextUri } = useSearchContext();
126
+ if (isPlaylistsLoading || !userPlaylists) {
127
+ return _jsx(Text, { dimColor: true, children: "Loading playlists..." });
128
+ }
129
+ if (userPlaylists.length === 0) {
130
+ return _jsx(Text, { dimColor: true, children: "No playlists found" });
131
+ }
132
+ const headerLines = 1;
133
+ const listHeight = Math.max(height - headerLines, 2);
134
+ const halfHeight = Math.floor(listHeight / 2);
135
+ const startIndex = Math.max(0, Math.min(selectedIndex - halfHeight, userPlaylists.length - listHeight));
136
+ const visibleCount = Math.min(listHeight, userPlaylists.length);
137
+ return (_jsxs(Box, { flexDirection: "column", height: height, overflow: "hidden", children: [_jsx(Box, { borderColor: "gray", borderStyle: "single", borderBottom: true, borderLeft: false, borderRight: false, borderTop: false, children: _jsx(Text, { bold: true, children: "Your Playlists" }) }), Array.from({
138
+ length: visibleCount,
139
+ }, (_, i) => {
140
+ const idx = startIndex + i;
141
+ const playlist = userPlaylists[idx];
142
+ if (!playlist)
143
+ return _jsx(Text, { children: " " }, idx);
144
+ const isSelected = idx === selectedIndex;
145
+ const isPlaying = currentContextUri !== null && playlist.uri === currentContextUri;
146
+ const icon = isPlaying ? '\u25B6 ' : isSelected ? '> ' : ' ';
147
+ if (isPlaying) {
148
+ return (_jsxs(Text, { bold: true, color: "green", inverse: isSelected, children: [icon, playlist.name, " (", playlist.totalTracks, " tracks)"] }, playlist.id));
149
+ }
150
+ return (_jsxs(Text, { inverse: isSelected, children: [icon, playlist.name, _jsxs(Text, { dimColor: true, children: [" (", playlist.totalTracks, " tracks)"] })] }, playlist.id));
151
+ })] }));
152
+ }
@@ -0,0 +1,12 @@
1
+ type ShimmerBarProps = {
2
+ width: number;
3
+ offset?: number;
4
+ };
5
+ /**
6
+ * Renders a horizontal shimmer placeholder bar with a sweeping highlight.
7
+ * Use for loading skeletons to avoid layout shifts when data arrives.
8
+ * @param width - Total width of the shimmer bar in characters
9
+ * @param offset - Optional phase offset for staggered animation across multiple bars
10
+ */
11
+ export declare function ShimmerBar({ width, offset }: ShimmerBarProps): import("react/jsx-runtime").JSX.Element;
12
+ export {};
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ import { useEffect, useState } from 'react';
4
+ const SHIMMER_CHAR = '\u2591'; // light shade block
5
+ const ANIMATION_INTERVAL_MS = 80;
6
+ const SHIMMER_WIDTH = 3;
7
+ /**
8
+ * Renders a horizontal shimmer placeholder bar with a sweeping highlight.
9
+ * Use for loading skeletons to avoid layout shifts when data arrives.
10
+ * @param width - Total width of the shimmer bar in characters
11
+ * @param offset - Optional phase offset for staggered animation across multiple bars
12
+ */
13
+ export function ShimmerBar({ width, offset = 0 }) {
14
+ const [tick, setTick] = useState(0);
15
+ useEffect(() => {
16
+ const id = setInterval(() => {
17
+ setTick((t) => t + 1);
18
+ }, ANIMATION_INTERVAL_MS);
19
+ return () => clearInterval(id);
20
+ }, []);
21
+ const barWidth = Math.max(width, 5);
22
+ const cycle = barWidth + SHIMMER_WIDTH;
23
+ const position = (tick + offset) % cycle;
24
+ let line = '';
25
+ for (let i = 0; i < barWidth; i++) {
26
+ line += SHIMMER_CHAR;
27
+ }
28
+ const highlightStart = Math.max(0, position - SHIMMER_WIDTH);
29
+ const highlightEnd = Math.min(barWidth, position);
30
+ const before = line.slice(0, highlightStart);
31
+ const highlight = line.slice(highlightStart, highlightEnd);
32
+ const after = line.slice(highlightEnd);
33
+ return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: before }), _jsx(Text, { color: "gray", children: highlight }), _jsx(Text, { dimColor: true, children: after })] }));
34
+ }
@@ -0,0 +1,12 @@
1
+ import type { ReactNode } from 'react';
2
+ type SidePanelProps = {
3
+ children: ReactNode;
4
+ width: number | undefined;
5
+ isWideLayout: boolean;
6
+ };
7
+ /**
8
+ * Shared wrapper for side panels (lyrics, queue). In wide layout, renders with
9
+ * a left border separator. In narrow layout, renders below content with margin.
10
+ */
11
+ export declare function SidePanel({ children, width, isWideLayout }: SidePanelProps): import("react/jsx-runtime").JSX.Element;
12
+ export {};
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box } from 'ink';
3
+ /**
4
+ * Shared wrapper for side panels (lyrics, queue). In wide layout, renders with
5
+ * a left border separator. In narrow layout, renders below content with margin.
6
+ */
7
+ export function SidePanel({ children, width, isWideLayout }) {
8
+ if (isWideLayout) {
9
+ return (_jsx(Box, { borderStyle: "single", borderLeft: true, borderTop: false, borderRight: false, borderBottom: false, borderColor: "gray", paddingLeft: 1, children: _jsx(Box, { flexDirection: "column", width: width, flexGrow: width ? 0 : 1, children: children }) }));
10
+ }
11
+ return (_jsx(Box, { flexDirection: "column", flexGrow: 1, marginTop: 1, children: children }));
12
+ }
@@ -0,0 +1,9 @@
1
+ type TrackInfoSkeletonProps = {
2
+ progressBarWidth: number;
3
+ };
4
+ /**
5
+ * Skeleton placeholder for TrackInfoView that mirrors its layout exactly.
6
+ * Prevents layout flickering when playback state transitions from loading to loaded.
7
+ */
8
+ export declare function TrackInfoSkeleton({ progressBarWidth }: TrackInfoSkeletonProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box } from 'ink';
3
+ import { ShimmerBar } from './shimmer.js';
4
+ /**
5
+ * Skeleton placeholder for TrackInfoView that mirrors its layout exactly.
6
+ * Prevents layout flickering when playback state transitions from loading to loaded.
7
+ */
8
+ export function TrackInfoSkeleton({ progressBarWidth }) {
9
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(ShimmerBar, { width: 24, offset: 0 }), _jsx(ShimmerBar, { width: 32, offset: 2 }), _jsx(Box, { marginTop: 1, children: _jsx(ShimmerBar, { width: progressBarWidth + 4, offset: 4 }) }), _jsx(Box, { marginTop: 1, children: _jsx(ShimmerBar, { width: 36, offset: 6 }) })] }));
10
+ }
@@ -0,0 +1,10 @@
1
+ import type { TrackInfo } from '../spotify/playback.js';
2
+ type TrackInfoProps = {
3
+ track: TrackInfo;
4
+ progressBarWidth: number;
5
+ };
6
+ /**
7
+ * Displays track name, artist, album, progress bar, and playback status.
8
+ */
9
+ export declare function TrackInfoView({ track, progressBarWidth }: TrackInfoProps): import("react/jsx-runtime").JSX.Element;
10
+ export {};
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { ProgressBar } from './progress-bar.js';
4
+ function formatTime(ms) {
5
+ const totalSeconds = Math.floor(ms / 1000);
6
+ const minutes = Math.floor(totalSeconds / 60);
7
+ const seconds = totalSeconds % 60;
8
+ return `${minutes}:${seconds.toString().padStart(2, '0')}`;
9
+ }
10
+ /**
11
+ * Displays track name, artist, album, progress bar, and playback status.
12
+ */
13
+ export function TrackInfoView({ track, progressBarWidth }) {
14
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Text, { bold: true, children: track.name }), _jsxs(Text, { children: [track.artist, track.album ? ` - ${track.album}` : ''] }), _jsxs(Box, { marginTop: 1, children: [track.isPlaying ? (_jsx(Text, { bold: true, color: "green", children: '\u25B6 ' })) : (_jsx(Text, { dimColor: true, children: '\u23F8 ' })), _jsx(ProgressBar, { progress: track.progressMs, duration: track.durationMs, width: progressBarWidth }), _jsxs(Text, { dimColor: true, children: [' ', formatTime(track.progressMs), " / ", formatTime(track.durationMs)] })] }), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsxs(Text, { dimColor: true, children: ["vol: ", track.volume ?? '?', "%"] }), _jsxs(Text, { dimColor: true, children: ["shuffle: ", track.shuffle ? 'on' : 'off'] }), _jsxs(Text, { dimColor: true, children: ["repeat: ", track.repeat] })] })] }));
15
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Default Spotify Client ID embedded in the binary.
3
+ * Users can override this at runtime via the `SPOTIFY_CLIENT_ID` environment variable.
4
+ * Leave empty to force users to provide their own Client ID.
5
+ */
6
+ export declare const DEFAULT_SPOTIFY_CLIENT_ID = "396cd16a363947ad95cb1032acdc4fc8";
7
+ /**
8
+ * User-Agent sent with every outgoing HTTP request (Spotify Web API + LRCLIB).
9
+ * Generated from package.json at runtime so it automatically tracks name/version.
10
+ * Shared via the `fetchWithRetry` wrapper which injects it into all requests.
11
+ */
12
+ export declare const USER_AGENT: string;
13
+ export declare const AUTH_SCOPES: string;
14
+ export declare const AUTH_REDIRECT_PORT = 8888;
15
+ export declare const AUTH_REDIRECT_URI = "http://127.0.0.1:8888/callback";
16
+ export declare const CONFIG_DIR: string;
17
+ export declare const TOKEN_PATH: string;
18
+ export declare const POLL_INTERVAL_MS = 10000;
19
+ export declare const TICK_INTERVAL_MS = 33;
20
+ export declare const SEEK_STEP_MS = 10000;
21
+ export declare const VOLUME_STEP = 5;
22
+ export declare const LYRICS_WIDE_COLUMNS = 80;
23
+ export declare const LYRICS_DEFAULT_OFFSET_MS = 100;
24
+ export declare const LYRICS_OFFSET_STEP_MS = 100;
25
+ export declare const SEARCH_DEBOUNCE_MS = 300;
26
+ /**
27
+ * Number of results returned per category by the search endpoint.
28
+ * Spotify documents max=50, but multi-type searches (track+album+artist+playlist)
29
+ * have an undocumented stricter cap that rejects larger values with
30
+ * `400 Invalid limit`. 20 is the documented default and is reliably accepted.
31
+ */
32
+ export declare const SEARCH_RESULTS_LIMIT = 20;
33
+ export declare const USER_PLAYLISTS_LIMIT = 50;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AA6BA;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,qCAAqC,CAAC;AAE5E;;;;GAIG;AACH,eAAO,MAAM,UAAU,QAA+F,CAAC;AAGvH,eAAO,MAAM,WAAW,QASb,CAAC;AACZ,eAAO,MAAM,kBAAkB,OAAO,CAAC;AACvC,eAAO,MAAM,iBAAiB,mCAAoD,CAAC;AAEnF,eAAO,MAAM,UAAU,QAAwC,CAAC;AAChE,eAAO,MAAM,UAAU,QAAkC,CAAC;AAE1D,eAAO,MAAM,gBAAgB,QAAS,CAAC;AACvC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,YAAY,QAAS,CAAC;AACnC,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,mBAAmB,KAAK,CAAC;AACtC,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAC5C,eAAO,MAAM,qBAAqB,MAAM,CAAC;AACzC,eAAO,MAAM,kBAAkB,MAAM,CAAC;AACtC;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,oBAAoB,KAAK,CAAC"}