@involvex/youtube-music-cli 0.0.47 → 0.0.49

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 (111) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/cli.js.map +6 -6
  3. package/dist/youtube-music-cli +0 -0
  4. package/package.json +1 -1
  5. package/dist/eslint.config.js +0 -55
  6. package/dist/package.json +0 -120
  7. package/dist/scripts/build-cli.js +0 -46
  8. package/dist/source/app.js +0 -17
  9. package/dist/source/cli.js +0 -504
  10. package/dist/source/components/common/ErrorBoundary.js +0 -22
  11. package/dist/source/components/common/Help.js +0 -18
  12. package/dist/source/components/common/ShortcutsBar.js +0 -89
  13. package/dist/source/components/config/ConfigLayout.js +0 -84
  14. package/dist/source/components/config/KeybindingsLayout.js +0 -107
  15. package/dist/source/components/export/ExportLayout.js +0 -111
  16. package/dist/source/components/import/ImportLayout.js +0 -119
  17. package/dist/source/components/import/ImportProgress.js +0 -73
  18. package/dist/source/components/layouts/ExploreLayout.js +0 -72
  19. package/dist/source/components/layouts/HistoryLayout.js +0 -37
  20. package/dist/source/components/layouts/LyricsLayout.js +0 -89
  21. package/dist/source/components/layouts/MainLayout.js +0 -190
  22. package/dist/source/components/layouts/MiniPlayerLayout.js +0 -20
  23. package/dist/source/components/layouts/PlayerLayout.js +0 -9
  24. package/dist/source/components/layouts/PluginsLayout.js +0 -77
  25. package/dist/source/components/layouts/SearchLayout.js +0 -193
  26. package/dist/source/components/layouts/TrendingLayout.js +0 -59
  27. package/dist/source/components/player/NowPlaying.js +0 -45
  28. package/dist/source/components/player/PlayerControls.js +0 -83
  29. package/dist/source/components/player/ProgressBar.js +0 -19
  30. package/dist/source/components/player/QueueList.js +0 -36
  31. package/dist/source/components/player/Suggestions.js +0 -50
  32. package/dist/source/components/playlist/PlaylistList.js +0 -138
  33. package/dist/source/components/plugins/PluginInstallDialog.js +0 -41
  34. package/dist/source/components/plugins/PluginsAvailable.js +0 -55
  35. package/dist/source/components/plugins/PluginsList.js +0 -18
  36. package/dist/source/components/search/SearchBar.js +0 -55
  37. package/dist/source/components/search/SearchHistory.js +0 -35
  38. package/dist/source/components/search/SearchResults.js +0 -280
  39. package/dist/source/components/settings/Settings.js +0 -211
  40. package/dist/source/components/theme/ThemeSwitcher.js +0 -11
  41. package/dist/source/config/themes.config.js +0 -123
  42. package/dist/source/contexts/theme.context.js +0 -29
  43. package/dist/source/hooks/useKeyboard.js +0 -188
  44. package/dist/source/hooks/useKeyboardBlocker.js +0 -45
  45. package/dist/source/hooks/useNavigation.js +0 -5
  46. package/dist/source/hooks/usePlayer.js +0 -43
  47. package/dist/source/hooks/usePlaylist.js +0 -65
  48. package/dist/source/hooks/useSearch.js +0 -76
  49. package/dist/source/hooks/useSleepTimer.js +0 -48
  50. package/dist/source/hooks/useTerminalSize.js +0 -24
  51. package/dist/source/hooks/useTheme.js +0 -5
  52. package/dist/source/hooks/useYouTubeMusic.js +0 -112
  53. package/dist/source/main.js +0 -127
  54. package/dist/source/services/cache/cache.service.js +0 -67
  55. package/dist/source/services/completions/completions.service.js +0 -313
  56. package/dist/source/services/config/config.service.js +0 -191
  57. package/dist/source/services/discord/discord-rpc.service.js +0 -95
  58. package/dist/source/services/download/download.service.js +0 -350
  59. package/dist/source/services/export/export.service.js +0 -131
  60. package/dist/source/services/history/history.service.js +0 -83
  61. package/dist/source/services/import/import.service.js +0 -272
  62. package/dist/source/services/import/spotify.service.js +0 -171
  63. package/dist/source/services/import/track-matcher.service.js +0 -271
  64. package/dist/source/services/import/youtube-import.service.js +0 -84
  65. package/dist/source/services/logger/logger.service.js +0 -52
  66. package/dist/source/services/lyrics/lyrics.service.js +0 -93
  67. package/dist/source/services/mpris/mpris.service.js +0 -78
  68. package/dist/source/services/notification/notification.service.js +0 -57
  69. package/dist/source/services/player/dependency-check.service.js +0 -140
  70. package/dist/source/services/player/player.service.js +0 -478
  71. package/dist/source/services/player-state/player-state.service.js +0 -123
  72. package/dist/source/services/plugin/plugin-audio-api.js +0 -36
  73. package/dist/source/services/plugin/plugin-context.js +0 -256
  74. package/dist/source/services/plugin/plugin-hooks.service.js +0 -135
  75. package/dist/source/services/plugin/plugin-installer.service.js +0 -248
  76. package/dist/source/services/plugin/plugin-loader.service.js +0 -161
  77. package/dist/source/services/plugin/plugin-permissions.service.js +0 -194
  78. package/dist/source/services/plugin/plugin-registry.service.js +0 -215
  79. package/dist/source/services/plugin/plugin-ui-api.js +0 -46
  80. package/dist/source/services/plugin/plugin-updater.service.js +0 -206
  81. package/dist/source/services/scrobbling/scrobbling.service.js +0 -115
  82. package/dist/source/services/sleep-timer/sleep-timer.service.js +0 -45
  83. package/dist/source/services/version-check/version-check.service.js +0 -121
  84. package/dist/source/services/web/static-file.service.js +0 -185
  85. package/dist/source/services/web/web-server-manager.js +0 -507
  86. package/dist/source/services/web/web-streaming.service.js +0 -292
  87. package/dist/source/services/web/websocket.server.js +0 -267
  88. package/dist/source/services/youtube-music/api.js +0 -649
  89. package/dist/source/services/youtube-music/search.service.js +0 -38
  90. package/dist/source/stores/history.store.js +0 -64
  91. package/dist/source/stores/navigation.store.js +0 -90
  92. package/dist/source/stores/player.store.js +0 -789
  93. package/dist/source/stores/plugins.store.js +0 -177
  94. package/dist/source/types/actions.js +0 -1
  95. package/dist/source/types/cli.types.js +0 -1
  96. package/dist/source/types/config.types.js +0 -1
  97. package/dist/source/types/history.types.js +0 -1
  98. package/dist/source/types/import.types.js +0 -2
  99. package/dist/source/types/keyboard.types.js +0 -1
  100. package/dist/source/types/navigation.types.js +0 -1
  101. package/dist/source/types/player.types.js +0 -1
  102. package/dist/source/types/playlist.types.js +0 -1
  103. package/dist/source/types/plugin.types.js +0 -1
  104. package/dist/source/types/theme.types.js +0 -1
  105. package/dist/source/types/web.types.js +0 -2
  106. package/dist/source/types/youtube-music.types.js +0 -1
  107. package/dist/source/types/youtubei.types.js +0 -3
  108. package/dist/source/utils/constants.js +0 -135
  109. package/dist/source/utils/format.js +0 -24
  110. package/dist/source/utils/icons.js +0 -28
  111. package/dist/source/utils/search-filters.js +0 -100
@@ -1,280 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // Search results component
3
- import React from 'react';
4
- import { Box, Text } from 'ink';
5
- import { useTheme } from "../../hooks/useTheme.js";
6
- import { useNavigation } from "../../hooks/useNavigation.js";
7
- import { useKeyBinding } from "../../hooks/useKeyboard.js";
8
- import { usePlayer } from "../../hooks/usePlayer.js";
9
- import { usePlaylist } from "../../hooks/usePlaylist.js";
10
- import { KEYBINDINGS } from "../../utils/constants.js";
11
- import { truncate } from "../../utils/format.js";
12
- import { useCallback, useRef, useEffect, useState } from 'react';
13
- import { logger } from "../../services/logger/logger.service.js";
14
- import { useTerminalSize } from "../../hooks/useTerminalSize.js";
15
- import { getMusicService } from "../../services/youtube-music/api.js";
16
- import { getDownloadService } from "../../services/download/download.service.js";
17
- // Generate unique component instance ID
18
- let instanceCounter = 0;
19
- function SearchResults({ results, selectedIndex, isActive = true, onMixCreated, onDownloadStatus, }) {
20
- const { theme } = useTheme();
21
- const { dispatch } = useNavigation();
22
- const { play, dispatch: playerDispatch } = usePlayer();
23
- const { columns } = useTerminalSize();
24
- const musicService = getMusicService();
25
- const downloadService = getDownloadService();
26
- const { createPlaylist } = usePlaylist();
27
- const [isDownloading, setIsDownloading] = useState(false);
28
- const mixCreatedRef = useRef(onMixCreated);
29
- mixCreatedRef.current = onMixCreated;
30
- const downloadStatusRef = useRef(onDownloadStatus);
31
- downloadStatusRef.current = onDownloadStatus;
32
- // Track component instance and last action time for debouncing
33
- const instanceIdRef = useRef(++instanceCounter);
34
- const lastSelectTime = useRef(0);
35
- const SELECT_DEBOUNCE_MS = 300; // Prevent duplicate triggers within 300ms
36
- useEffect(() => {
37
- const instanceId = instanceIdRef.current;
38
- logger.debug('SearchResults', 'Component mounted', { instanceId });
39
- return () => {
40
- logger.debug('SearchResults', 'Component unmounted', { instanceId });
41
- };
42
- }, []);
43
- // Navigate results with arrow keys
44
- const navigateUp = useCallback(() => {
45
- if (!isActive)
46
- return;
47
- if (selectedIndex > 0) {
48
- dispatch({ category: 'SET_SELECTED_RESULT', index: selectedIndex - 1 });
49
- }
50
- }, [selectedIndex, dispatch, isActive]);
51
- const navigateDown = useCallback(() => {
52
- if (!isActive)
53
- return;
54
- if (selectedIndex < results.length - 1) {
55
- dispatch({ category: 'SET_SELECTED_RESULT', index: selectedIndex + 1 });
56
- }
57
- }, [selectedIndex, results.length, dispatch, isActive]);
58
- // Play selected result
59
- const playSelected = useCallback(async () => {
60
- logger.debug('SearchResults', 'playSelected called', {
61
- isActive,
62
- selectedIndex,
63
- resultsLength: results.length,
64
- });
65
- if (!isActive)
66
- return;
67
- const selected = results[selectedIndex];
68
- logger.info('SearchResults', 'Playing selected track', {
69
- type: selected?.type,
70
- title: selected?.type === 'song' ? selected.data.title : 'N/A',
71
- });
72
- if (selected && selected.type === 'song') {
73
- // Clear queue when playing from search results to ensure indices match
74
- play(selected.data, { clearQueue: true });
75
- }
76
- else if (selected && selected.type === 'artist') {
77
- const artistName = 'name' in selected.data ? selected.data.name : '';
78
- if (!artistName) {
79
- logger.warn('SearchResults', 'Artist name missing, cannot search songs');
80
- return;
81
- }
82
- try {
83
- const response = await musicService.search(artistName, {
84
- type: 'songs',
85
- limit: 20,
86
- });
87
- const tracks = response.results
88
- .filter(result => result.type === 'song')
89
- .map(result => result.data);
90
- if (tracks.length === 0) {
91
- logger.warn('SearchResults', 'No songs found for artist', {
92
- artistName,
93
- });
94
- return;
95
- }
96
- // Replace queue with artist songs and start playback
97
- playerDispatch({ category: 'CLEAR_QUEUE' });
98
- playerDispatch({ category: 'SET_QUEUE', queue: tracks });
99
- playerDispatch({ category: 'PLAY', track: tracks[0] });
100
- }
101
- catch (error) {
102
- logger.error('SearchResults', 'Failed to play artist songs', {
103
- error,
104
- });
105
- }
106
- }
107
- else {
108
- logger.warn('SearchResults', 'Selected item is not playable', {
109
- type: selected?.type,
110
- });
111
- }
112
- }, [selectedIndex, results, play, isActive, musicService, playerDispatch]);
113
- // Play selected result handler (memoized to prevent duplicate registrations)
114
- const handleSelect = useCallback(() => {
115
- const now = Date.now();
116
- const timeSinceLastSelect = now - lastSelectTime.current;
117
- const instanceId = instanceIdRef.current;
118
- if (!isActive) {
119
- logger.debug('SearchResults', 'SELECT ignored, not active', { instanceId });
120
- return;
121
- }
122
- // Debounce to prevent double-triggers
123
- if (timeSinceLastSelect < SELECT_DEBOUNCE_MS) {
124
- logger.warn('SearchResults', 'SELECT debounced (duplicate trigger)', {
125
- instanceId,
126
- timeSinceLastSelect,
127
- debounceMs: SELECT_DEBOUNCE_MS,
128
- });
129
- return;
130
- }
131
- lastSelectTime.current = now;
132
- logger.debug('SearchResults', 'SELECT key pressed', { isActive, instanceId });
133
- playSelected();
134
- }, [isActive, playSelected]);
135
- const createMixPlaylist = useCallback(async () => {
136
- if (!isActive)
137
- return;
138
- const selected = results[selectedIndex];
139
- if (!selected) {
140
- logger.warn('SearchResults', 'No result selected for mix');
141
- return;
142
- }
143
- let playlistName = 'Dynamic mix';
144
- const collectedTracks = [];
145
- if (selected.type === 'song') {
146
- const selectedTrack = selected.data;
147
- const title = selectedTrack.title || 'selected track';
148
- playlistName = `Mix for ${title}`;
149
- collectedTracks.push(selectedTrack);
150
- try {
151
- const suggestions = await musicService.getSuggestions(selectedTrack.videoId);
152
- collectedTracks.push(...suggestions);
153
- }
154
- catch (error) {
155
- logger.error('SearchResults', 'Failed to fetch song suggestions', {
156
- error,
157
- });
158
- }
159
- }
160
- else if (selected.type === 'artist') {
161
- const artistName = 'name' in selected.data ? selected.data.name : '';
162
- if (!artistName) {
163
- logger.warn('SearchResults', 'Artist name missing for mix');
164
- mixCreatedRef.current?.('Artist information is missing, cannot create mix.');
165
- return;
166
- }
167
- playlistName = `${artistName} mix`;
168
- try {
169
- const response = await musicService.search(artistName, {
170
- type: 'songs',
171
- limit: 25,
172
- });
173
- const artistTracks = response.results
174
- .filter(result => result.type === 'song')
175
- .map(result => result.data);
176
- collectedTracks.push(...artistTracks);
177
- }
178
- catch (error) {
179
- logger.error('SearchResults', 'Failed to fetch artist songs for mix', {
180
- error,
181
- });
182
- }
183
- }
184
- else {
185
- logger.warn('SearchResults', 'Mix creation unsupported result type', {
186
- type: selected.type,
187
- });
188
- mixCreatedRef.current?.('Mix creation is only supported for songs and artists.');
189
- return;
190
- }
191
- const uniqueTracks = [];
192
- const seenVideoIds = new Set();
193
- for (const track of collectedTracks) {
194
- if (!track?.videoId || seenVideoIds.has(track.videoId))
195
- continue;
196
- seenVideoIds.add(track.videoId);
197
- uniqueTracks.push(track);
198
- }
199
- if (uniqueTracks.length === 0) {
200
- mixCreatedRef.current?.('No similar tracks were found to create a mix.');
201
- return;
202
- }
203
- const playlist = createPlaylist(playlistName, uniqueTracks);
204
- logger.info('SearchResults', 'Mix playlist created', {
205
- name: playlist.name,
206
- trackCount: uniqueTracks.length,
207
- });
208
- // Queue the mix tracks and start playing the first one
209
- playerDispatch({ category: 'SET_QUEUE', queue: uniqueTracks });
210
- const firstTrack = uniqueTracks[0];
211
- if (firstTrack) {
212
- playerDispatch({ category: 'PLAY', track: firstTrack });
213
- }
214
- mixCreatedRef.current?.(`Created mix "${playlist.name}" with ${uniqueTracks.length} tracks — playing now (Esc to go back).`);
215
- }, [
216
- createPlaylist,
217
- isActive,
218
- musicService,
219
- playerDispatch,
220
- results,
221
- selectedIndex,
222
- ]);
223
- const downloadSelected = useCallback(async () => {
224
- if (!isActive)
225
- return;
226
- if (isDownloading) {
227
- downloadStatusRef.current?.('Download already in progress. Please wait.');
228
- return;
229
- }
230
- const selected = results[selectedIndex];
231
- if (!selected)
232
- return;
233
- const config = downloadService.getConfig();
234
- if (!config.enabled) {
235
- downloadStatusRef.current?.('Downloads are disabled. Enable Download Feature in Settings.');
236
- return;
237
- }
238
- try {
239
- setIsDownloading(true);
240
- const target = await downloadService.resolveSearchTarget(selected);
241
- if (target.tracks.length === 0) {
242
- downloadStatusRef.current?.(`No tracks found for "${target.name}".`);
243
- return;
244
- }
245
- downloadStatusRef.current?.(`Downloading ${target.tracks.length} track(s) from "${target.name}"... this can take a few minutes.`);
246
- const summary = await downloadService.downloadTracks(target.tracks);
247
- downloadStatusRef.current?.(`Downloaded ${summary.downloaded}, skipped ${summary.skipped}, failed ${summary.failed}.`);
248
- }
249
- catch (error) {
250
- downloadStatusRef.current?.(error instanceof Error ? error.message : 'Download failed.');
251
- }
252
- finally {
253
- setIsDownloading(false);
254
- }
255
- }, [downloadService, isActive, isDownloading, results, selectedIndex]);
256
- useKeyBinding(KEYBINDINGS.UP, navigateUp);
257
- useKeyBinding(KEYBINDINGS.DOWN, navigateDown);
258
- useKeyBinding(KEYBINDINGS.SELECT, handleSelect);
259
- useKeyBinding(KEYBINDINGS.CREATE_MIX, () => {
260
- void createMixPlaylist();
261
- });
262
- useKeyBinding(KEYBINDINGS.DOWNLOAD, () => {
263
- void downloadSelected();
264
- });
265
- // Note: Removed redundant useEffect that was syncing selectedIndex to dispatch
266
- // This was causing unnecessary re-renders. The selectedIndex is already managed
267
- // by the parent component (SearchLayout) and passed down as a prop.
268
- if (results.length === 0) {
269
- return null;
270
- }
271
- // Calculate responsive truncation
272
- const maxTitleWidth = Math.max(20, Math.floor(columns * 0.4));
273
- return (_jsx(Box, { flexDirection: "column", children: results.map((result, index) => {
274
- const isSelected = index === selectedIndex;
275
- const data = result.data;
276
- const title = 'title' in data ? data.title : 'name' in data ? data.name : 'Unknown';
277
- return (_jsxs(Box, { paddingX: 1, backgroundColor: isSelected ? theme.colors.secondary : undefined, children: [_jsx(Text, { color: isSelected ? theme.colors.primary : theme.colors.dim, bold: isSelected, children: (isSelected ? '> ' : ' ') + (index + 1).toString().padEnd(4) }), _jsx(Text, { color: isSelected ? theme.colors.primary : theme.colors.dim, bold: isSelected, children: result.type.toUpperCase().padEnd(10) }), _jsx(Text, { color: isSelected ? theme.colors.primary : theme.colors.text, bold: isSelected, children: truncate(title, maxTitleWidth) })] }, index));
278
- }) }));
279
- }
280
- export default React.memo(SearchResults);
@@ -1,211 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // Settings component
3
- import { useState } from 'react';
4
- import { Box, Text } from 'ink';
5
- import TextInput from 'ink-text-input';
6
- import { useTheme } from "../../hooks/useTheme.js";
7
- import { useNavigation } from "../../hooks/useNavigation.js";
8
- import { getConfigService } from "../../services/config/config.service.js";
9
- import { useKeyBinding } from "../../hooks/useKeyboard.js";
10
- import { KEYBINDINGS, VIEW } from "../../utils/constants.js";
11
- import { useSleepTimer } from "../../hooks/useSleepTimer.js";
12
- import { formatTime } from "../../utils/format.js";
13
- import { useKeyboardBlocker } from "../../hooks/useKeyboardBlocker.js";
14
- const QUALITIES = ['low', 'medium', 'high'];
15
- const DOWNLOAD_FORMATS = ['mp3', 'm4a'];
16
- const CROSSFADE_PRESETS = [0, 1, 2, 3, 5];
17
- const EQUALIZER_PRESETS = [
18
- 'flat',
19
- 'bass_boost',
20
- 'vocal',
21
- 'bright',
22
- 'warm',
23
- ];
24
- const VOLUME_FADE_PRESETS = [0, 1, 2, 3, 5];
25
- const SETTINGS_ITEMS = [
26
- 'Stream Quality',
27
- 'Audio Normalization',
28
- 'Gapless Playback',
29
- 'Crossfade Duration',
30
- 'Volume Fade Duration',
31
- 'Equalizer Preset',
32
- 'Notifications',
33
- 'Discord Rich Presence',
34
- 'Downloads Enabled',
35
- 'Download Folder',
36
- 'Download Format',
37
- 'Sleep Timer',
38
- 'Import Playlists',
39
- 'Export Playlists',
40
- 'Custom Keybindings',
41
- 'Manage Plugins',
42
- ];
43
- export default function Settings() {
44
- const { theme } = useTheme();
45
- const { dispatch } = useNavigation();
46
- const config = getConfigService();
47
- const [selectedIndex, setSelectedIndex] = useState(0);
48
- const [quality, setQuality] = useState(config.get('streamQuality') || 'high');
49
- const [audioNormalization, setAudioNormalization] = useState(config.get('audioNormalization') ?? false);
50
- const [gaplessPlayback, setGaplessPlayback] = useState(config.get('gaplessPlayback') ?? true);
51
- const [crossfadeDuration, setCrossfadeDuration] = useState(config.get('crossfadeDuration') ?? 0);
52
- const [volumeFadeDuration, setVolumeFadeDuration] = useState(config.get('volumeFadeDuration') ?? 0);
53
- const [equalizerPreset, setEqualizerPreset] = useState(config.get('equalizerPreset') ?? 'flat');
54
- const [notifications, setNotifications] = useState(config.get('notifications') ?? false);
55
- const [discordRpc, setDiscordRpc] = useState(config.get('discordRichPresence') ?? false);
56
- const [downloadsEnabled, setDownloadsEnabled] = useState(config.get('downloadsEnabled') ?? false);
57
- const [downloadDirectory, setDownloadDirectory] = useState(config.get('downloadDirectory') ?? '');
58
- const [downloadFormat, setDownloadFormat] = useState(config.get('downloadFormat') ?? 'mp3');
59
- const [isEditingDownloadDirectory, setIsEditingDownloadDirectory] = useState(false);
60
- const { isActive, activeMinutes, remainingSeconds, startTimer, cancelTimer, presets, } = useSleepTimer();
61
- useKeyboardBlocker(isEditingDownloadDirectory);
62
- const navigateUp = () => {
63
- setSelectedIndex(prev => Math.max(0, prev - 1));
64
- };
65
- const navigateDown = () => {
66
- setSelectedIndex(prev => Math.min(SETTINGS_ITEMS.length - 1, prev + 1));
67
- };
68
- const toggleQuality = () => {
69
- const currentIndex = QUALITIES.indexOf(quality);
70
- const nextQuality = QUALITIES[(currentIndex + 1) % QUALITIES.length];
71
- setQuality(nextQuality);
72
- config.set('streamQuality', nextQuality);
73
- };
74
- const toggleNormalization = () => {
75
- const next = !audioNormalization;
76
- setAudioNormalization(next);
77
- config.set('audioNormalization', next);
78
- };
79
- const toggleGaplessPlayback = () => {
80
- const next = !gaplessPlayback;
81
- setGaplessPlayback(next);
82
- config.set('gaplessPlayback', next);
83
- };
84
- const cycleCrossfadeDuration = () => {
85
- const currentIndex = CROSSFADE_PRESETS.indexOf(crossfadeDuration);
86
- const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % CROSSFADE_PRESETS.length;
87
- const next = CROSSFADE_PRESETS[nextIndex] ?? 0;
88
- setCrossfadeDuration(next);
89
- config.set('crossfadeDuration', next);
90
- };
91
- const cycleVolumeFadeDuration = () => {
92
- const currentIndex = VOLUME_FADE_PRESETS.indexOf(volumeFadeDuration);
93
- const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % VOLUME_FADE_PRESETS.length;
94
- const next = VOLUME_FADE_PRESETS[nextIndex] ?? 0;
95
- setVolumeFadeDuration(next);
96
- config.set('volumeFadeDuration', next);
97
- };
98
- const cycleEqualizerPreset = () => {
99
- const currentIndex = EQUALIZER_PRESETS.indexOf(equalizerPreset);
100
- const nextPreset = EQUALIZER_PRESETS[(currentIndex + 1) % EQUALIZER_PRESETS.length];
101
- setEqualizerPreset(nextPreset);
102
- config.set('equalizerPreset', nextPreset);
103
- };
104
- const formatEqualizerLabel = (preset) => preset
105
- .split('_')
106
- .map(segment => `${segment.charAt(0).toUpperCase()}${segment.slice(1)}`)
107
- .join(' ');
108
- const toggleNotifications = () => {
109
- const next = !notifications;
110
- setNotifications(next);
111
- config.set('notifications', next);
112
- };
113
- const toggleDiscordRpc = () => {
114
- const next = !discordRpc;
115
- setDiscordRpc(next);
116
- config.set('discordRichPresence', next);
117
- };
118
- const toggleDownloadsEnabled = () => {
119
- const next = !downloadsEnabled;
120
- setDownloadsEnabled(next);
121
- config.set('downloadsEnabled', next);
122
- };
123
- const cycleDownloadFormat = () => {
124
- const currentIndex = DOWNLOAD_FORMATS.indexOf(downloadFormat);
125
- const nextFormat = DOWNLOAD_FORMATS[(currentIndex + 1) % DOWNLOAD_FORMATS.length];
126
- setDownloadFormat(nextFormat);
127
- config.set('downloadFormat', nextFormat);
128
- };
129
- const cycleSleepTimer = () => {
130
- if (isActive) {
131
- cancelTimer();
132
- return;
133
- }
134
- // Find next preset (start from first if none active)
135
- const currentPresetIndex = activeMinutes
136
- ? presets.indexOf(activeMinutes)
137
- : -1;
138
- const nextPreset = presets[(currentPresetIndex + 1) % presets.length];
139
- startTimer(nextPreset);
140
- };
141
- const handleSelect = () => {
142
- if (selectedIndex === 0) {
143
- toggleQuality();
144
- }
145
- else if (selectedIndex === 1) {
146
- toggleNormalization();
147
- }
148
- else if (selectedIndex === 2) {
149
- toggleGaplessPlayback();
150
- }
151
- else if (selectedIndex === 3) {
152
- cycleCrossfadeDuration();
153
- }
154
- else if (selectedIndex === 4) {
155
- cycleVolumeFadeDuration();
156
- }
157
- else if (selectedIndex === 5) {
158
- cycleEqualizerPreset();
159
- }
160
- else if (selectedIndex === 6) {
161
- toggleNotifications();
162
- }
163
- else if (selectedIndex === 7) {
164
- toggleDiscordRpc();
165
- }
166
- else if (selectedIndex === 8) {
167
- toggleDownloadsEnabled();
168
- }
169
- else if (selectedIndex === 9) {
170
- setIsEditingDownloadDirectory(true);
171
- }
172
- else if (selectedIndex === 10) {
173
- cycleDownloadFormat();
174
- }
175
- else if (selectedIndex === 11) {
176
- cycleSleepTimer();
177
- }
178
- else if (selectedIndex === 12) {
179
- dispatch({ category: 'NAVIGATE', view: VIEW.IMPORT });
180
- }
181
- else if (selectedIndex === 13) {
182
- dispatch({ category: 'NAVIGATE', view: VIEW.EXPORT_PLAYLISTS });
183
- }
184
- else if (selectedIndex === 14) {
185
- dispatch({ category: 'NAVIGATE', view: VIEW.KEYBINDINGS });
186
- }
187
- else if (selectedIndex === 15) {
188
- dispatch({ category: 'NAVIGATE', view: VIEW.PLUGINS });
189
- }
190
- };
191
- useKeyBinding(KEYBINDINGS.UP, navigateUp);
192
- useKeyBinding(KEYBINDINGS.DOWN, navigateDown);
193
- useKeyBinding(KEYBINDINGS.SELECT, handleSelect);
194
- const sleepTimerLabel = isActive && remainingSeconds !== null
195
- ? `Sleep Timer: ${formatTime(remainingSeconds)} remaining (Enter to cancel)`
196
- : 'Sleep Timer: Off (Enter to set)';
197
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { borderStyle: "double", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Settings" }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 0 ? theme.colors.primary : undefined, color: selectedIndex === 0 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 0, children: ["Stream Quality: ", quality.toUpperCase()] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 1 ? theme.colors.primary : undefined, color: selectedIndex === 1 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 1, children: ["Audio Normalization: ", audioNormalization ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 2 ? theme.colors.primary : undefined, color: selectedIndex === 2 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 2, children: ["Gapless Playback: ", gaplessPlayback ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 3 ? theme.colors.primary : undefined, color: selectedIndex === 3 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 3, children: ["Crossfade: ", crossfadeDuration === 0 ? 'Off' : `${crossfadeDuration}s`] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 4 ? theme.colors.primary : undefined, color: selectedIndex === 4 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 4, children: ["Volume Fade:", ' ', volumeFadeDuration === 0 ? 'Off' : `${volumeFadeDuration}s`] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 5 ? theme.colors.primary : undefined, color: selectedIndex === 5 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 5, children: ["Equalizer: ", formatEqualizerLabel(equalizerPreset)] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 6 ? theme.colors.primary : undefined, color: selectedIndex === 6 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 6, children: ["Desktop Notifications: ", notifications ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 7 ? theme.colors.primary : undefined, color: selectedIndex === 7 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 7, children: ["Discord Rich Presence: ", discordRpc ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 8 ? theme.colors.primary : undefined, color: selectedIndex === 8 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 8, children: ["Download Feature: ", downloadsEnabled ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: isEditingDownloadDirectory && selectedIndex === 9 ? (_jsx(TextInput, { value: downloadDirectory, onChange: setDownloadDirectory, onSubmit: value => {
198
- const normalized = value.trim();
199
- if (!normalized) {
200
- setIsEditingDownloadDirectory(false);
201
- return;
202
- }
203
- setDownloadDirectory(normalized);
204
- config.set('downloadDirectory', normalized);
205
- setIsEditingDownloadDirectory(false);
206
- }, placeholder: "Download directory", focus: true })) : (_jsxs(Text, { backgroundColor: selectedIndex === 9 ? theme.colors.primary : undefined, color: selectedIndex === 9 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 9, children: ["Download Folder: ", downloadDirectory] })) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 10 ? theme.colors.primary : undefined, color: selectedIndex === 10 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 10, children: ["Download Format: ", downloadFormat.toUpperCase()] }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 11 ? theme.colors.primary : undefined, color: selectedIndex === 11
207
- ? theme.colors.background
208
- : isActive
209
- ? theme.colors.accent
210
- : theme.colors.text, bold: selectedIndex === 11, children: sleepTimerLabel }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 12 ? theme.colors.primary : undefined, color: selectedIndex === 12 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 12, children: "Import Playlists \u2192" }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 13 ? theme.colors.primary : undefined, color: selectedIndex === 13 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 13, children: "Export Playlists \u2192" }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 14 ? theme.colors.primary : undefined, color: selectedIndex === 14 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 14, children: "Custom Keybindings \u2192" }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 15 ? theme.colors.primary : undefined, color: selectedIndex === 15 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 15, children: "Manage Plugins" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.dim, children: "Arrows to navigate, Enter to select, Esc/q to go back" }) })] }));
211
- }
@@ -1,11 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- // Theme switcher component
3
- import { Box, Text } from 'ink';
4
- import { useTheme } from "../../hooks/useTheme.js";
5
- import { BUILTIN_THEMES } from "../../config/themes.config.js";
6
- import { useState } from 'react';
7
- export default function ThemeSwitcher() {
8
- const { theme } = useTheme();
9
- const [expanded, _setExpanded] = useState(false);
10
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { borderStyle: "double", borderColor: theme.colors.secondary, paddingX: 1, children: [_jsxs(Text, { bold: true, color: theme.colors.primary, children: ["Theme: ", theme.name] }), _jsx(Text, { children: " " }), _jsx(Text, { color: theme.colors.dim, children: "(Enter to change, Esc to close)" })] }), expanded ? (_jsx(_Fragment, { children: Object.keys(BUILTIN_THEMES).map(themeName => (_jsxs(Box, { paddingX: 2, children: [_jsx(Text, { color: theme.colors.text, children: "\u2192 " }), _jsx(Text, { color: theme.colors.dim, children: themeName }), _jsx(Text, { children: " " }), _jsxs(Text, { color: theme.colors.dim, children: ["Press ", _jsx(Text, { color: theme.colors.text, children: "Enter" }), " to select"] })] }, themeName))) })) : (_jsxs(Box, { paddingX: 2, children: [_jsx(Text, { color: theme.colors.dim, children: "Press " }), _jsx(Text, { color: theme.colors.text, children: "Enter" }), _jsx(Text, { color: theme.colors.dim, children: " to browse themes" })] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.colors.dim, children: ["Current: ", _jsx(Text, { color: theme.colors.primary, children: theme.name })] }) })] }));
11
- }
@@ -1,123 +0,0 @@
1
- export const BUILTIN_THEMES = {
2
- dark: {
3
- name: 'Dark',
4
- colors: {
5
- primary: 'cyan',
6
- secondary: 'blue',
7
- background: 'black',
8
- text: 'white',
9
- accent: 'yellow',
10
- dim: 'gray',
11
- error: 'red',
12
- success: 'green',
13
- warning: 'yellow',
14
- },
15
- inverse: false,
16
- },
17
- light: {
18
- name: 'Light',
19
- colors: {
20
- primary: 'blue',
21
- secondary: 'cyan',
22
- background: 'white',
23
- text: 'black',
24
- accent: 'magenta',
25
- dim: 'gray',
26
- error: 'red',
27
- success: 'green',
28
- warning: 'yellow',
29
- },
30
- inverse: false,
31
- },
32
- midnight: {
33
- name: 'Midnight',
34
- colors: {
35
- primary: 'magenta',
36
- secondary: 'purple',
37
- background: 'black',
38
- text: 'white',
39
- accent: 'cyan',
40
- dim: 'gray',
41
- error: 'red',
42
- success: 'greenBright',
43
- warning: 'yellowBright',
44
- },
45
- inverse: false,
46
- },
47
- matrix: {
48
- name: 'Matrix',
49
- colors: {
50
- primary: 'green',
51
- secondary: 'greenBright',
52
- background: 'black',
53
- text: 'white',
54
- accent: 'greenBright',
55
- dim: 'green',
56
- error: 'red',
57
- success: 'greenBright',
58
- warning: 'yellow',
59
- },
60
- inverse: false,
61
- },
62
- dracula: {
63
- name: 'Dracula',
64
- colors: {
65
- primary: 'magenta',
66
- secondary: 'cyan',
67
- background: 'black',
68
- text: 'white',
69
- accent: 'yellow',
70
- dim: 'gray',
71
- error: 'red',
72
- success: 'green',
73
- warning: 'yellow',
74
- },
75
- inverse: false,
76
- },
77
- nord: {
78
- name: 'Nord',
79
- colors: {
80
- primary: 'blue',
81
- secondary: 'cyan',
82
- background: 'black',
83
- text: 'white',
84
- accent: 'blueBright',
85
- dim: 'gray',
86
- error: 'red',
87
- success: 'greenBright',
88
- warning: 'yellow',
89
- },
90
- inverse: false,
91
- },
92
- solarized: {
93
- name: 'Solarized',
94
- colors: {
95
- primary: 'cyan',
96
- secondary: 'blue',
97
- background: 'black',
98
- text: 'white',
99
- accent: 'yellow',
100
- dim: 'gray',
101
- error: 'red',
102
- success: 'green',
103
- warning: 'magenta',
104
- },
105
- inverse: false,
106
- },
107
- catppuccin: {
108
- name: 'Catppuccin',
109
- colors: {
110
- primary: 'magenta',
111
- secondary: 'blue',
112
- background: 'black',
113
- text: 'white',
114
- accent: 'cyan',
115
- dim: 'gray',
116
- error: 'red',
117
- success: 'green',
118
- warning: 'yellow',
119
- },
120
- inverse: false,
121
- },
122
- };
123
- export const DEFAULT_THEME = BUILTIN_THEMES['dark'];
@@ -1,29 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- // Theme context and provider
3
- import { createContext, useContext, useState, useCallback, } from 'react';
4
- import { getConfigService } from "../services/config/config.service.js";
5
- const ThemeContext = createContext(null);
6
- export function ThemeProvider({ children }) {
7
- const [theme, setThemeState] = useState(getConfigService().getTheme());
8
- const [themeName, setThemeNameState] = useState(getConfigService().get('theme'));
9
- const setTheme = useCallback((name) => {
10
- const configService = getConfigService();
11
- configService.updateTheme(name);
12
- setThemeNameState(name);
13
- setThemeState(configService.getTheme());
14
- }, []);
15
- const setCustomTheme = useCallback((themeValue) => {
16
- const configService = getConfigService();
17
- configService.setCustomTheme(themeValue);
18
- setThemeNameState('custom');
19
- setThemeState(themeValue);
20
- }, []);
21
- return (_jsx(ThemeContext.Provider, { value: { theme, themeName, setTheme, setCustomTheme }, children: children }));
22
- }
23
- export function useTheme() {
24
- const context = useContext(ThemeContext);
25
- if (!context) {
26
- throw new Error('useTheme must be used within ThemeProvider');
27
- }
28
- return context;
29
- }