@involvex/youtube-music-cli 0.0.46 → 0.0.48

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 (118) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/cli.js.map +1004 -0
  3. package/dist/source/hooks/usePlayer.d.ts +1 -0
  4. package/dist/source/services/player-state/player-state.service.d.ts +1 -0
  5. package/dist/source/stores/player.store.d.ts +1 -0
  6. package/dist/source/types/actions.d.ts +4 -0
  7. package/dist/source/types/player.types.d.ts +3 -2
  8. package/dist/source/utils/constants.d.ts +1 -0
  9. package/dist/source/utils/icons.d.ts +1 -0
  10. package/dist/youtube-music-cli +0 -0
  11. package/package.json +1 -1
  12. package/dist/eslint.config.js +0 -55
  13. package/dist/package.json +0 -120
  14. package/dist/scripts/build-cli.js +0 -46
  15. package/dist/source/app.js +0 -17
  16. package/dist/source/cli.js +0 -504
  17. package/dist/source/components/common/ErrorBoundary.js +0 -22
  18. package/dist/source/components/common/Help.js +0 -18
  19. package/dist/source/components/common/ShortcutsBar.js +0 -80
  20. package/dist/source/components/config/ConfigLayout.js +0 -84
  21. package/dist/source/components/config/KeybindingsLayout.js +0 -107
  22. package/dist/source/components/export/ExportLayout.js +0 -111
  23. package/dist/source/components/import/ImportLayout.js +0 -119
  24. package/dist/source/components/import/ImportProgress.js +0 -73
  25. package/dist/source/components/layouts/ExploreLayout.js +0 -72
  26. package/dist/source/components/layouts/HistoryLayout.js +0 -37
  27. package/dist/source/components/layouts/LyricsLayout.js +0 -89
  28. package/dist/source/components/layouts/MainLayout.js +0 -190
  29. package/dist/source/components/layouts/MiniPlayerLayout.js +0 -20
  30. package/dist/source/components/layouts/PlayerLayout.js +0 -9
  31. package/dist/source/components/layouts/PluginsLayout.js +0 -77
  32. package/dist/source/components/layouts/SearchLayout.js +0 -193
  33. package/dist/source/components/layouts/TrendingLayout.js +0 -59
  34. package/dist/source/components/player/NowPlaying.js +0 -45
  35. package/dist/source/components/player/PlayerControls.js +0 -83
  36. package/dist/source/components/player/ProgressBar.js +0 -19
  37. package/dist/source/components/player/QueueList.js +0 -36
  38. package/dist/source/components/player/Suggestions.js +0 -50
  39. package/dist/source/components/playlist/PlaylistList.js +0 -138
  40. package/dist/source/components/plugins/PluginInstallDialog.js +0 -41
  41. package/dist/source/components/plugins/PluginsAvailable.js +0 -55
  42. package/dist/source/components/plugins/PluginsList.js +0 -18
  43. package/dist/source/components/search/SearchBar.js +0 -55
  44. package/dist/source/components/search/SearchHistory.js +0 -35
  45. package/dist/source/components/search/SearchResults.js +0 -280
  46. package/dist/source/components/settings/Settings.js +0 -211
  47. package/dist/source/components/theme/ThemeSwitcher.js +0 -11
  48. package/dist/source/config/themes.config.js +0 -123
  49. package/dist/source/contexts/theme.context.js +0 -29
  50. package/dist/source/hooks/useKeyboard.js +0 -188
  51. package/dist/source/hooks/useKeyboardBlocker.js +0 -45
  52. package/dist/source/hooks/useNavigation.js +0 -5
  53. package/dist/source/hooks/usePlayer.js +0 -43
  54. package/dist/source/hooks/usePlaylist.js +0 -65
  55. package/dist/source/hooks/useSearch.js +0 -76
  56. package/dist/source/hooks/useSleepTimer.js +0 -48
  57. package/dist/source/hooks/useTerminalSize.js +0 -24
  58. package/dist/source/hooks/useTheme.js +0 -5
  59. package/dist/source/hooks/useYouTubeMusic.js +0 -112
  60. package/dist/source/main.js +0 -127
  61. package/dist/source/services/cache/cache.service.js +0 -67
  62. package/dist/source/services/completions/completions.service.js +0 -313
  63. package/dist/source/services/config/config.service.js +0 -191
  64. package/dist/source/services/discord/discord-rpc.service.js +0 -95
  65. package/dist/source/services/download/download.service.js +0 -350
  66. package/dist/source/services/export/export.service.js +0 -131
  67. package/dist/source/services/history/history.service.js +0 -83
  68. package/dist/source/services/import/import.service.js +0 -272
  69. package/dist/source/services/import/spotify.service.js +0 -171
  70. package/dist/source/services/import/track-matcher.service.js +0 -271
  71. package/dist/source/services/import/youtube-import.service.js +0 -84
  72. package/dist/source/services/logger/logger.service.js +0 -52
  73. package/dist/source/services/lyrics/lyrics.service.js +0 -93
  74. package/dist/source/services/mpris/mpris.service.js +0 -78
  75. package/dist/source/services/notification/notification.service.js +0 -57
  76. package/dist/source/services/player/dependency-check.service.js +0 -140
  77. package/dist/source/services/player/player.service.js +0 -478
  78. package/dist/source/services/player-state/player-state.service.js +0 -122
  79. package/dist/source/services/plugin/plugin-audio-api.js +0 -36
  80. package/dist/source/services/plugin/plugin-context.js +0 -256
  81. package/dist/source/services/plugin/plugin-hooks.service.js +0 -135
  82. package/dist/source/services/plugin/plugin-installer.service.js +0 -248
  83. package/dist/source/services/plugin/plugin-loader.service.js +0 -161
  84. package/dist/source/services/plugin/plugin-permissions.service.js +0 -194
  85. package/dist/source/services/plugin/plugin-registry.service.js +0 -215
  86. package/dist/source/services/plugin/plugin-ui-api.js +0 -46
  87. package/dist/source/services/plugin/plugin-updater.service.js +0 -206
  88. package/dist/source/services/scrobbling/scrobbling.service.js +0 -115
  89. package/dist/source/services/sleep-timer/sleep-timer.service.js +0 -45
  90. package/dist/source/services/version-check/version-check.service.js +0 -121
  91. package/dist/source/services/web/static-file.service.js +0 -185
  92. package/dist/source/services/web/web-server-manager.js +0 -506
  93. package/dist/source/services/web/web-streaming.service.js +0 -290
  94. package/dist/source/services/web/websocket.server.js +0 -267
  95. package/dist/source/services/youtube-music/api.js +0 -649
  96. package/dist/source/services/youtube-music/search.service.js +0 -38
  97. package/dist/source/stores/history.store.js +0 -64
  98. package/dist/source/stores/navigation.store.js +0 -90
  99. package/dist/source/stores/player.store.js +0 -724
  100. package/dist/source/stores/plugins.store.js +0 -177
  101. package/dist/source/types/actions.js +0 -1
  102. package/dist/source/types/cli.types.js +0 -1
  103. package/dist/source/types/config.types.js +0 -1
  104. package/dist/source/types/history.types.js +0 -1
  105. package/dist/source/types/import.types.js +0 -2
  106. package/dist/source/types/keyboard.types.js +0 -1
  107. package/dist/source/types/navigation.types.js +0 -1
  108. package/dist/source/types/player.types.js +0 -1
  109. package/dist/source/types/playlist.types.js +0 -1
  110. package/dist/source/types/plugin.types.js +0 -1
  111. package/dist/source/types/theme.types.js +0 -1
  112. package/dist/source/types/web.types.js +0 -2
  113. package/dist/source/types/youtube-music.types.js +0 -1
  114. package/dist/source/types/youtubei.types.js +0 -3
  115. package/dist/source/utils/constants.js +0 -134
  116. package/dist/source/utils/format.js +0 -24
  117. package/dist/source/utils/icons.js +0 -26
  118. 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
- }