@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.
- package/CHANGELOG.md +8 -0
- package/dist/cli.js.map +6 -6
- package/dist/youtube-music-cli +0 -0
- package/package.json +1 -1
- package/dist/eslint.config.js +0 -55
- package/dist/package.json +0 -120
- package/dist/scripts/build-cli.js +0 -46
- package/dist/source/app.js +0 -17
- package/dist/source/cli.js +0 -504
- package/dist/source/components/common/ErrorBoundary.js +0 -22
- package/dist/source/components/common/Help.js +0 -18
- package/dist/source/components/common/ShortcutsBar.js +0 -89
- package/dist/source/components/config/ConfigLayout.js +0 -84
- package/dist/source/components/config/KeybindingsLayout.js +0 -107
- package/dist/source/components/export/ExportLayout.js +0 -111
- package/dist/source/components/import/ImportLayout.js +0 -119
- package/dist/source/components/import/ImportProgress.js +0 -73
- package/dist/source/components/layouts/ExploreLayout.js +0 -72
- package/dist/source/components/layouts/HistoryLayout.js +0 -37
- package/dist/source/components/layouts/LyricsLayout.js +0 -89
- package/dist/source/components/layouts/MainLayout.js +0 -190
- package/dist/source/components/layouts/MiniPlayerLayout.js +0 -20
- package/dist/source/components/layouts/PlayerLayout.js +0 -9
- package/dist/source/components/layouts/PluginsLayout.js +0 -77
- package/dist/source/components/layouts/SearchLayout.js +0 -193
- package/dist/source/components/layouts/TrendingLayout.js +0 -59
- package/dist/source/components/player/NowPlaying.js +0 -45
- package/dist/source/components/player/PlayerControls.js +0 -83
- package/dist/source/components/player/ProgressBar.js +0 -19
- package/dist/source/components/player/QueueList.js +0 -36
- package/dist/source/components/player/Suggestions.js +0 -50
- package/dist/source/components/playlist/PlaylistList.js +0 -138
- package/dist/source/components/plugins/PluginInstallDialog.js +0 -41
- package/dist/source/components/plugins/PluginsAvailable.js +0 -55
- package/dist/source/components/plugins/PluginsList.js +0 -18
- package/dist/source/components/search/SearchBar.js +0 -55
- package/dist/source/components/search/SearchHistory.js +0 -35
- package/dist/source/components/search/SearchResults.js +0 -280
- package/dist/source/components/settings/Settings.js +0 -211
- package/dist/source/components/theme/ThemeSwitcher.js +0 -11
- package/dist/source/config/themes.config.js +0 -123
- package/dist/source/contexts/theme.context.js +0 -29
- package/dist/source/hooks/useKeyboard.js +0 -188
- package/dist/source/hooks/useKeyboardBlocker.js +0 -45
- package/dist/source/hooks/useNavigation.js +0 -5
- package/dist/source/hooks/usePlayer.js +0 -43
- package/dist/source/hooks/usePlaylist.js +0 -65
- package/dist/source/hooks/useSearch.js +0 -76
- package/dist/source/hooks/useSleepTimer.js +0 -48
- package/dist/source/hooks/useTerminalSize.js +0 -24
- package/dist/source/hooks/useTheme.js +0 -5
- package/dist/source/hooks/useYouTubeMusic.js +0 -112
- package/dist/source/main.js +0 -127
- package/dist/source/services/cache/cache.service.js +0 -67
- package/dist/source/services/completions/completions.service.js +0 -313
- package/dist/source/services/config/config.service.js +0 -191
- package/dist/source/services/discord/discord-rpc.service.js +0 -95
- package/dist/source/services/download/download.service.js +0 -350
- package/dist/source/services/export/export.service.js +0 -131
- package/dist/source/services/history/history.service.js +0 -83
- package/dist/source/services/import/import.service.js +0 -272
- package/dist/source/services/import/spotify.service.js +0 -171
- package/dist/source/services/import/track-matcher.service.js +0 -271
- package/dist/source/services/import/youtube-import.service.js +0 -84
- package/dist/source/services/logger/logger.service.js +0 -52
- package/dist/source/services/lyrics/lyrics.service.js +0 -93
- package/dist/source/services/mpris/mpris.service.js +0 -78
- package/dist/source/services/notification/notification.service.js +0 -57
- package/dist/source/services/player/dependency-check.service.js +0 -140
- package/dist/source/services/player/player.service.js +0 -478
- package/dist/source/services/player-state/player-state.service.js +0 -123
- package/dist/source/services/plugin/plugin-audio-api.js +0 -36
- package/dist/source/services/plugin/plugin-context.js +0 -256
- package/dist/source/services/plugin/plugin-hooks.service.js +0 -135
- package/dist/source/services/plugin/plugin-installer.service.js +0 -248
- package/dist/source/services/plugin/plugin-loader.service.js +0 -161
- package/dist/source/services/plugin/plugin-permissions.service.js +0 -194
- package/dist/source/services/plugin/plugin-registry.service.js +0 -215
- package/dist/source/services/plugin/plugin-ui-api.js +0 -46
- package/dist/source/services/plugin/plugin-updater.service.js +0 -206
- package/dist/source/services/scrobbling/scrobbling.service.js +0 -115
- package/dist/source/services/sleep-timer/sleep-timer.service.js +0 -45
- package/dist/source/services/version-check/version-check.service.js +0 -121
- package/dist/source/services/web/static-file.service.js +0 -185
- package/dist/source/services/web/web-server-manager.js +0 -507
- package/dist/source/services/web/web-streaming.service.js +0 -292
- package/dist/source/services/web/websocket.server.js +0 -267
- package/dist/source/services/youtube-music/api.js +0 -649
- package/dist/source/services/youtube-music/search.service.js +0 -38
- package/dist/source/stores/history.store.js +0 -64
- package/dist/source/stores/navigation.store.js +0 -90
- package/dist/source/stores/player.store.js +0 -789
- package/dist/source/stores/plugins.store.js +0 -177
- package/dist/source/types/actions.js +0 -1
- package/dist/source/types/cli.types.js +0 -1
- package/dist/source/types/config.types.js +0 -1
- package/dist/source/types/history.types.js +0 -1
- package/dist/source/types/import.types.js +0 -2
- package/dist/source/types/keyboard.types.js +0 -1
- package/dist/source/types/navigation.types.js +0 -1
- package/dist/source/types/player.types.js +0 -1
- package/dist/source/types/playlist.types.js +0 -1
- package/dist/source/types/plugin.types.js +0 -1
- package/dist/source/types/theme.types.js +0 -1
- package/dist/source/types/web.types.js +0 -2
- package/dist/source/types/youtube-music.types.js +0 -1
- package/dist/source/types/youtubei.types.js +0 -3
- package/dist/source/utils/constants.js +0 -135
- package/dist/source/utils/format.js +0 -24
- package/dist/source/utils/icons.js +0 -28
- package/dist/source/utils/search-filters.js +0 -100
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// Now playing component
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
import { usePlayer } from "../../hooks/usePlayer.js";
|
|
5
|
-
import { useTheme } from "../../hooks/useTheme.js";
|
|
6
|
-
import { formatTime } from "../../utils/format.js";
|
|
7
|
-
import { useTerminalSize } from "../../hooks/useTerminalSize.js";
|
|
8
|
-
import { getSleepTimerService } from "../../services/sleep-timer/sleep-timer.service.js";
|
|
9
|
-
import { useState, useEffect } from 'react';
|
|
10
|
-
import { ICONS } from "../../utils/icons.js";
|
|
11
|
-
export default function NowPlaying() {
|
|
12
|
-
const { theme } = useTheme();
|
|
13
|
-
const { state: playerState } = usePlayer();
|
|
14
|
-
const { columns } = useTerminalSize();
|
|
15
|
-
const sleepTimer = getSleepTimerService();
|
|
16
|
-
const [sleepRemaining, setSleepRemaining] = useState(null);
|
|
17
|
-
// Poll sleep timer remaining every second
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
if (!sleepTimer.isActive()) {
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
const interval = setInterval(() => {
|
|
23
|
-
const remaining = sleepTimer.getRemainingSeconds();
|
|
24
|
-
setSleepRemaining(remaining);
|
|
25
|
-
if (remaining === null || remaining === 0) {
|
|
26
|
-
clearInterval(interval);
|
|
27
|
-
}
|
|
28
|
-
}, 1000);
|
|
29
|
-
return () => {
|
|
30
|
-
clearInterval(interval);
|
|
31
|
-
};
|
|
32
|
-
}, [sleepTimer]);
|
|
33
|
-
if (!playerState.currentTrack) {
|
|
34
|
-
return (_jsx(Box, { borderStyle: "round", borderColor: theme.colors.dim, paddingX: 1, children: _jsx(Text, { color: theme.colors.dim, children: "No track playing" }) }));
|
|
35
|
-
}
|
|
36
|
-
const track = playerState.currentTrack;
|
|
37
|
-
const artists = track.artists?.map(a => a.name).join(', ') || 'Unknown Artist';
|
|
38
|
-
// Clamp progress to valid range
|
|
39
|
-
const progress = Math.max(0, Math.min(playerState.progress, playerState.duration || 0));
|
|
40
|
-
const duration = playerState.duration || 0;
|
|
41
|
-
const percentage = duration > 0 ? Math.min(100, Math.floor((progress / duration) * 100)) : 0;
|
|
42
|
-
const barWidth = Math.max(10, columns - 8);
|
|
43
|
-
const filledWidth = duration > 0 ? Math.floor((progress / duration) * barWidth) : 0;
|
|
44
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.colors.primary, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: track.title }), _jsx(Text, { color: theme.colors.dim, children: " \u2022 " }), _jsx(Text, { color: theme.colors.secondary, children: artists })] }), track.album && _jsx(Text, { color: theme.colors.dim, children: track.album.name }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.primary, children: '█'.repeat(Math.min(filledWidth, barWidth)) }), _jsx(Text, { color: theme.colors.dim, children: '░'.repeat(Math.max(0, barWidth - filledWidth)) })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.text, children: formatTime(progress) }), _jsxs(Text, { color: theme.colors.dim, children: [" / ", formatTime(duration), " "] }), _jsxs(Text, { color: theme.colors.dim, children: ["[", percentage, "%]"] }), playerState.isLoading && (_jsx(Text, { color: theme.colors.accent, children: " Loading..." })), !playerState.isPlaying && progress > 0 && (_jsxs(Text, { color: theme.colors.dim, children: [" ", ICONS.PAUSE] })), playerState.shuffle && (_jsxs(Text, { color: theme.colors.primary, children: [" ", ICONS.SHUFFLE] })), sleepRemaining !== null && (_jsxs(Text, { color: theme.colors.warning, children: [' ', "\u23FE ", formatTime(sleepRemaining)] }))] }), playerState.error && (_jsx(Text, { color: theme.colors.error, children: playerState.error }))] }));
|
|
45
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// Player controls component
|
|
3
|
-
import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
4
|
-
import { KEYBINDINGS } from "../../utils/constants.js";
|
|
5
|
-
import { usePlayer } from "../../hooks/usePlayer.js";
|
|
6
|
-
import { useTheme } from "../../hooks/useTheme.js";
|
|
7
|
-
import { Box, Text } from 'ink';
|
|
8
|
-
import { useEffect, useState } from 'react';
|
|
9
|
-
import { logger } from "../../services/logger/logger.service.js";
|
|
10
|
-
import { ICONS } from "../../utils/icons.js";
|
|
11
|
-
import { getConfigService } from "../../services/config/config.service.js";
|
|
12
|
-
let mountCount = 0;
|
|
13
|
-
const CROSSFADE_PRESETS = [0, 1, 2, 3, 5];
|
|
14
|
-
const EQUALIZER_PRESETS = [
|
|
15
|
-
'flat',
|
|
16
|
-
'bass_boost',
|
|
17
|
-
'vocal',
|
|
18
|
-
'bright',
|
|
19
|
-
'warm',
|
|
20
|
-
];
|
|
21
|
-
const formatEqualizerLabel = (preset) => preset
|
|
22
|
-
.split('_')
|
|
23
|
-
.map(segment => `${segment.charAt(0).toUpperCase()}${segment.slice(1)}`)
|
|
24
|
-
.join(' ');
|
|
25
|
-
export default function PlayerControls() {
|
|
26
|
-
const instanceId = ++mountCount;
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
logger.debug('PlayerControls', 'Component mounted', { instanceId });
|
|
29
|
-
return () => {
|
|
30
|
-
logger.debug('PlayerControls', 'Component unmounted', { instanceId });
|
|
31
|
-
};
|
|
32
|
-
}, [instanceId]);
|
|
33
|
-
const { theme } = useTheme();
|
|
34
|
-
const { state: playerState, pause, resume, next, previous, volumeUp, volumeDown, speedUp, speedDown, toggleShuffle, } = usePlayer();
|
|
35
|
-
const config = getConfigService();
|
|
36
|
-
const [gaplessPlayback, setGaplessPlayback] = useState(config.get('gaplessPlayback') ?? true);
|
|
37
|
-
const [crossfadeDuration, setCrossfadeDuration] = useState(config.get('crossfadeDuration') ?? 0);
|
|
38
|
-
const [equalizerPreset, setEqualizerPreset] = useState(config.get('equalizerPreset') ?? 'flat');
|
|
39
|
-
// DEBUG: Log when callbacks change (detect instability)
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
// Temporarily output to stderr to debug without triggering Ink re-render
|
|
42
|
-
process.stderr.write(`[PlayerControls] volumeUp callback: ${typeof volumeUp}\n`);
|
|
43
|
-
}, [volumeUp, instanceId]);
|
|
44
|
-
const handlePlayPause = () => {
|
|
45
|
-
if (playerState.isPlaying) {
|
|
46
|
-
pause();
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
resume();
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
const toggleGaplessPlayback = () => {
|
|
53
|
-
const next = !gaplessPlayback;
|
|
54
|
-
setGaplessPlayback(next);
|
|
55
|
-
config.set('gaplessPlayback', next);
|
|
56
|
-
};
|
|
57
|
-
const cycleCrossfadeDuration = () => {
|
|
58
|
-
const currentIndex = CROSSFADE_PRESETS.indexOf(crossfadeDuration);
|
|
59
|
-
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % CROSSFADE_PRESETS.length;
|
|
60
|
-
const next = CROSSFADE_PRESETS[nextIndex] ?? 0;
|
|
61
|
-
setCrossfadeDuration(next);
|
|
62
|
-
config.set('crossfadeDuration', next);
|
|
63
|
-
};
|
|
64
|
-
const cycleEqualizerPreset = () => {
|
|
65
|
-
const currentIndex = EQUALIZER_PRESETS.indexOf(equalizerPreset);
|
|
66
|
-
const next = EQUALIZER_PRESETS[(currentIndex + 1) % EQUALIZER_PRESETS.length];
|
|
67
|
-
setEqualizerPreset(next);
|
|
68
|
-
config.set('equalizerPreset', next);
|
|
69
|
-
};
|
|
70
|
-
// Keyboard bindings
|
|
71
|
-
useKeyBinding(KEYBINDINGS.PLAY_PAUSE, handlePlayPause);
|
|
72
|
-
useKeyBinding(KEYBINDINGS.NEXT, next);
|
|
73
|
-
useKeyBinding(KEYBINDINGS.PREVIOUS, previous);
|
|
74
|
-
useKeyBinding(KEYBINDINGS.VOLUME_UP, volumeUp);
|
|
75
|
-
useKeyBinding(KEYBINDINGS.VOLUME_DOWN, volumeDown);
|
|
76
|
-
useKeyBinding(KEYBINDINGS.SPEED_UP, speedUp);
|
|
77
|
-
useKeyBinding(KEYBINDINGS.SPEED_DOWN, speedDown);
|
|
78
|
-
useKeyBinding(KEYBINDINGS.SHUFFLE, toggleShuffle);
|
|
79
|
-
useKeyBinding(KEYBINDINGS.GAPLESS_TOGGLE, toggleGaplessPlayback);
|
|
80
|
-
useKeyBinding(KEYBINDINGS.CROSSFADE_CYCLE, cycleCrossfadeDuration);
|
|
81
|
-
useKeyBinding(KEYBINDINGS.EQUALIZER_CYCLE, cycleEqualizerPreset);
|
|
82
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", paddingX: 2, borderStyle: "classic", borderColor: theme.colors.dim, children: [_jsxs(Text, { color: theme.colors.text, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "\u2190 / b" }), "] Prev"] }), _jsx(Text, { color: theme.colors.primary, children: playerState.isPlaying ? (_jsxs(Text, { children: ["[", _jsx(Text, { color: theme.colors.dim, children: "Space" }), "] Pause"] })) : (_jsxs(Text, { children: ["[", _jsx(Text, { color: theme.colors.dim, children: "Space" }), "] Play"] })) }), _jsxs(Text, { color: theme.colors.text, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "\u2192 / n" }), "] Next"] }), _jsxs(Text, { color: theme.colors.text, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "+/-" }), "] Vol: ", playerState.volume, "%"] }), _jsxs(Text, { color: playerState.shuffle ? theme.colors.primary : theme.colors.dim, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "Shift+S" }), "]", ' ', playerState.shuffle ? `${ICONS.SHUFFLE} ON` : `${ICONS.SHUFFLE} OFF`] }), (playerState.speed ?? 1.0) !== 1.0 && (_jsxs(Text, { color: theme.colors.accent, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "<>" }), "]", ' ', (playerState.speed ?? 1.0).toFixed(2), "x"] }))] }), _jsxs(Box, { flexDirection: "row", justifyContent: "space-between", paddingX: 2, gap: 2, children: [_jsxs(Text, { color: gaplessPlayback ? theme.colors.primary : theme.colors.dim, children: ["Gapless: ", gaplessPlayback ? 'ON' : 'OFF'] }), _jsxs(Text, { color: theme.colors.text, children: ["Crossfade: ", crossfadeDuration === 0 ? 'Off' : `${crossfadeDuration}s`] }), _jsxs(Text, { color: theme.colors.text, children: ["Equalizer: ", formatEqualizerLabel(equalizerPreset)] })] })] }));
|
|
83
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// Progress bar component
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
import { useTheme } from "../../hooks/useTheme.js";
|
|
5
|
-
import { usePlayer } from "../../hooks/usePlayer.js";
|
|
6
|
-
import { formatTime } from "../../utils/format.js";
|
|
7
|
-
export default function ProgressBar() {
|
|
8
|
-
const { theme } = useTheme();
|
|
9
|
-
const { state: playerState } = usePlayer();
|
|
10
|
-
if (!playerState.currentTrack || !playerState.duration) {
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
13
|
-
// Clamp values to valid range
|
|
14
|
-
const progress = Math.max(0, Math.min(playerState.progress, playerState.duration));
|
|
15
|
-
const duration = playerState.duration;
|
|
16
|
-
const percentage = duration > 0 ? Math.min(100, Math.floor((progress / duration) * 100)) : 0;
|
|
17
|
-
const barWidth = Math.min(20, Math.floor(percentage / 5));
|
|
18
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.text, children: formatTime(progress) }), _jsx(Text, { color: theme.colors.dim, children: "/" }), _jsx(Text, { color: theme.colors.text, children: formatTime(duration) }), _jsx(Text, { children: " " }), _jsx(Text, { color: theme.colors.primary, children: '█'.repeat(barWidth) }), _jsx(Text, { color: theme.colors.dim, children: '░'.repeat(20 - barWidth) }), _jsxs(Text, { color: theme.colors.dim, children: [" ", percentage, "%"] })] }));
|
|
19
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
// Queue management component
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import React from 'react';
|
|
5
|
-
import { Box, Text } from 'ink';
|
|
6
|
-
import { useTheme } from "../../hooks/useTheme.js";
|
|
7
|
-
import { usePlayer } from "../../hooks/usePlayer.js";
|
|
8
|
-
import { truncate } from "../../utils/format.js";
|
|
9
|
-
import { useTerminalSize } from "../../hooks/useTerminalSize.js";
|
|
10
|
-
function QueueList() {
|
|
11
|
-
const { theme } = useTheme();
|
|
12
|
-
const { state: playerState } = usePlayer();
|
|
13
|
-
const { columns } = useTerminalSize();
|
|
14
|
-
const [selectedIndex, _setSelectedIndex] = useState(0);
|
|
15
|
-
// Calculate responsive truncation
|
|
16
|
-
const getTruncateLength = (baseLength) => {
|
|
17
|
-
const scale = Math.min(1, columns / 100);
|
|
18
|
-
return Math.max(20, Math.floor(baseLength * scale));
|
|
19
|
-
};
|
|
20
|
-
if (playerState.queue.length === 0) {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
// Show only next 5 tracks
|
|
24
|
-
const visibleQueue = playerState.queue.slice(playerState.queuePosition + 1, playerState.queuePosition + 6);
|
|
25
|
-
if (visibleQueue.length === 0) {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.dim, children: ["Up next (", playerState.queue.length - playerState.queuePosition - 1, ' ', "tracks)"] }), visibleQueue.map((track, idx) => {
|
|
29
|
-
const index = playerState.queuePosition + 1 + idx;
|
|
30
|
-
const isSelected = index === selectedIndex;
|
|
31
|
-
const artists = track.artists?.map(a => a.name).join(', ') || 'Unknown';
|
|
32
|
-
const title = truncate(track.title, getTruncateLength(40));
|
|
33
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.dim, children: [index + 1, ". "] }), _jsx(Text, { color: isSelected ? theme.colors.primary : theme.colors.text, children: title }), _jsxs(Text, { color: theme.colors.dim, children: [" \u2022 ", artists] })] }, track.videoId));
|
|
34
|
-
})] }));
|
|
35
|
-
}
|
|
36
|
-
export default React.memo(QueueList);
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// Suggestions component
|
|
3
|
-
import { useEffect, useState, useCallback } from 'react';
|
|
4
|
-
import { Box, Text } from 'ink';
|
|
5
|
-
import { useYouTubeMusic } from "../../hooks/useYouTubeMusic.js";
|
|
6
|
-
import { usePlayer } from "../../hooks/usePlayer.js";
|
|
7
|
-
import { useTheme } from "../../hooks/useTheme.js";
|
|
8
|
-
import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
9
|
-
import { KEYBINDINGS } from "../../utils/constants.js";
|
|
10
|
-
import { truncate } from "../../utils/format.js";
|
|
11
|
-
export default function Suggestions() {
|
|
12
|
-
const { theme } = useTheme();
|
|
13
|
-
const { state: playerState, play } = usePlayer();
|
|
14
|
-
const { getSuggestions, isLoading } = useYouTubeMusic();
|
|
15
|
-
const [suggestions, setSuggestions] = useState([]);
|
|
16
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
if (playerState.currentTrack?.videoId) {
|
|
19
|
-
getSuggestions(playerState.currentTrack.videoId).then(tracks => {
|
|
20
|
-
setSuggestions(tracks);
|
|
21
|
-
setSelectedIndex(0);
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
}, [playerState.currentTrack?.videoId, getSuggestions]);
|
|
25
|
-
const navigateUp = useCallback(() => {
|
|
26
|
-
setSelectedIndex(prev => Math.max(0, prev - 1));
|
|
27
|
-
}, []);
|
|
28
|
-
const navigateDown = useCallback(() => {
|
|
29
|
-
setSelectedIndex(prev => Math.min(suggestions.length - 1, prev + 1));
|
|
30
|
-
}, [suggestions.length]);
|
|
31
|
-
const playSelected = useCallback(() => {
|
|
32
|
-
const track = suggestions[selectedIndex];
|
|
33
|
-
if (track) {
|
|
34
|
-
play(track);
|
|
35
|
-
}
|
|
36
|
-
}, [selectedIndex, suggestions, play]);
|
|
37
|
-
useKeyBinding(KEYBINDINGS.UP, navigateUp);
|
|
38
|
-
useKeyBinding(KEYBINDINGS.DOWN, navigateDown);
|
|
39
|
-
useKeyBinding(KEYBINDINGS.SELECT, playSelected);
|
|
40
|
-
if (isLoading) {
|
|
41
|
-
return _jsx(Text, { color: theme.colors.accent, children: "Loading suggestions..." });
|
|
42
|
-
}
|
|
43
|
-
if (suggestions.length === 0) {
|
|
44
|
-
return _jsx(Text, { color: theme.colors.dim, children: "No suggestions available" });
|
|
45
|
-
}
|
|
46
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { bold: true, color: theme.colors.primary, children: ["Suggestions based on: ", playerState.currentTrack?.title] }), suggestions.map((track, index) => {
|
|
47
|
-
const isSelected = index === selectedIndex;
|
|
48
|
-
return (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: isSelected ? theme.colors.primary : undefined, color: isSelected ? theme.colors.background : theme.colors.text, bold: isSelected, children: [index + 1, ". ", truncate(track.title, 40), " -", ' ', track.artists?.map(a => a.name).join(', ')] }) }, track.videoId));
|
|
49
|
-
}), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.dim, children: "Arrows to navigate, Enter to play, Esc to go back" }) })] }));
|
|
50
|
-
}
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// Playlist list component
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
import TextInput from 'ink-text-input';
|
|
5
|
-
import { useCallback, useState } from 'react';
|
|
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 { useTheme } from "../../hooks/useTheme.js";
|
|
11
|
-
import { useKeyboardBlocker } from "../../hooks/useKeyboardBlocker.js";
|
|
12
|
-
import { KEYBINDINGS } from "../../utils/constants.js";
|
|
13
|
-
import { getDownloadService } from "../../services/download/download.service.js";
|
|
14
|
-
export default function PlaylistList() {
|
|
15
|
-
const { theme } = useTheme();
|
|
16
|
-
const { play, setQueue } = usePlayer();
|
|
17
|
-
const { dispatch } = useNavigation();
|
|
18
|
-
const downloadService = getDownloadService();
|
|
19
|
-
const { playlists, createPlaylist, renamePlaylist, deletePlaylist } = usePlaylist();
|
|
20
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
21
|
-
const [lastCreated, setLastCreated] = useState(null);
|
|
22
|
-
const [renamingPlaylistId, setRenamingPlaylistId] = useState(null);
|
|
23
|
-
const [renameValue, setRenameValue] = useState('');
|
|
24
|
-
const [downloadStatus, setDownloadStatus] = useState(null);
|
|
25
|
-
const [isDownloading, setIsDownloading] = useState(false);
|
|
26
|
-
useKeyboardBlocker(renamingPlaylistId !== null);
|
|
27
|
-
const handleCreate = useCallback(() => {
|
|
28
|
-
const name = `Playlist ${playlists.length + 1}`;
|
|
29
|
-
const playlist = createPlaylist(name);
|
|
30
|
-
setLastCreated(playlist.name);
|
|
31
|
-
setSelectedIndex(playlists.length);
|
|
32
|
-
}, [createPlaylist, playlists.length]);
|
|
33
|
-
const navigateUp = useCallback(() => {
|
|
34
|
-
setSelectedIndex(prev => Math.max(0, prev - 1));
|
|
35
|
-
}, []);
|
|
36
|
-
const navigateDown = useCallback(() => {
|
|
37
|
-
setSelectedIndex(prev => Math.min(playlists.length === 0 ? 0 : playlists.length - 1, prev + 1));
|
|
38
|
-
}, [playlists.length]);
|
|
39
|
-
const startPlaylist = useCallback(() => {
|
|
40
|
-
if (renamingPlaylistId)
|
|
41
|
-
return;
|
|
42
|
-
const playlist = playlists[selectedIndex];
|
|
43
|
-
if (!playlist || playlist.tracks.length === 0)
|
|
44
|
-
return;
|
|
45
|
-
setQueue([...playlist.tracks]);
|
|
46
|
-
const firstTrack = playlist.tracks[0];
|
|
47
|
-
if (!firstTrack)
|
|
48
|
-
return;
|
|
49
|
-
play(firstTrack);
|
|
50
|
-
}, [play, playlists, selectedIndex, renamingPlaylistId, setQueue]);
|
|
51
|
-
const handleRename = useCallback(() => {
|
|
52
|
-
const playlist = playlists[selectedIndex];
|
|
53
|
-
if (!playlist)
|
|
54
|
-
return;
|
|
55
|
-
setRenamingPlaylistId(playlist.playlistId);
|
|
56
|
-
setRenameValue(playlist.name);
|
|
57
|
-
}, [playlists, selectedIndex]);
|
|
58
|
-
const handleRenameSubmit = useCallback((value) => {
|
|
59
|
-
if (!renamingPlaylistId)
|
|
60
|
-
return;
|
|
61
|
-
const trimmedValue = value.trim() || `Playlist ${selectedIndex + 1}`;
|
|
62
|
-
renamePlaylist(renamingPlaylistId, trimmedValue);
|
|
63
|
-
setRenamingPlaylistId(null);
|
|
64
|
-
setRenameValue('');
|
|
65
|
-
}, [renamePlaylist, renamingPlaylistId, selectedIndex]);
|
|
66
|
-
const handleBack = useCallback(() => {
|
|
67
|
-
if (renamingPlaylistId) {
|
|
68
|
-
setRenamingPlaylistId(null);
|
|
69
|
-
setRenameValue('');
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
dispatch({ category: 'GO_BACK' });
|
|
73
|
-
}, [dispatch, renamingPlaylistId]);
|
|
74
|
-
const handleDelete = useCallback(() => {
|
|
75
|
-
if (renamingPlaylistId)
|
|
76
|
-
return;
|
|
77
|
-
const playlist = playlists[selectedIndex];
|
|
78
|
-
if (!playlist)
|
|
79
|
-
return;
|
|
80
|
-
deletePlaylist(playlist.playlistId);
|
|
81
|
-
setSelectedIndex(prev => Math.max(0, prev - 1));
|
|
82
|
-
}, [deletePlaylist, playlists, renamingPlaylistId, selectedIndex]);
|
|
83
|
-
const handleDownload = useCallback(async () => {
|
|
84
|
-
if (renamingPlaylistId)
|
|
85
|
-
return;
|
|
86
|
-
if (isDownloading) {
|
|
87
|
-
setDownloadStatus('Download already in progress. Please wait.');
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const playlist = playlists[selectedIndex];
|
|
91
|
-
if (!playlist)
|
|
92
|
-
return;
|
|
93
|
-
const config = downloadService.getConfig();
|
|
94
|
-
if (!config.enabled) {
|
|
95
|
-
setDownloadStatus('Downloads are disabled. Enable Download Feature in Settings.');
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
const target = downloadService.resolvePlaylistTarget(playlist);
|
|
99
|
-
if (target.tracks.length === 0) {
|
|
100
|
-
setDownloadStatus(`No tracks to download in "${playlist.name}".`);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
setDownloadStatus(`Downloading ${target.tracks.length} track(s) from "${playlist.name}"... this can take a few minutes.`);
|
|
104
|
-
try {
|
|
105
|
-
setIsDownloading(true);
|
|
106
|
-
const summary = await downloadService.downloadTracks(target.tracks);
|
|
107
|
-
setDownloadStatus(`Downloaded ${summary.downloaded}, skipped ${summary.skipped}, failed ${summary.failed}.`);
|
|
108
|
-
}
|
|
109
|
-
catch (error) {
|
|
110
|
-
setDownloadStatus(error instanceof Error ? error.message : 'Failed to download playlist.');
|
|
111
|
-
}
|
|
112
|
-
finally {
|
|
113
|
-
setIsDownloading(false);
|
|
114
|
-
}
|
|
115
|
-
}, [
|
|
116
|
-
downloadService,
|
|
117
|
-
isDownloading,
|
|
118
|
-
playlists,
|
|
119
|
-
renamingPlaylistId,
|
|
120
|
-
selectedIndex,
|
|
121
|
-
]);
|
|
122
|
-
useKeyBinding(KEYBINDINGS.UP, navigateUp);
|
|
123
|
-
useKeyBinding(KEYBINDINGS.DOWN, navigateDown);
|
|
124
|
-
useKeyBinding(KEYBINDINGS.SELECT, startPlaylist);
|
|
125
|
-
useKeyBinding(['r'], handleRename);
|
|
126
|
-
useKeyBinding(KEYBINDINGS.CREATE_PLAYLIST, handleCreate);
|
|
127
|
-
useKeyBinding(KEYBINDINGS.DELETE_PLAYLIST, handleDelete);
|
|
128
|
-
useKeyBinding(KEYBINDINGS.BACK, handleBack);
|
|
129
|
-
useKeyBinding(KEYBINDINGS.DOWNLOAD, () => {
|
|
130
|
-
void handleDownload();
|
|
131
|
-
});
|
|
132
|
-
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: "Playlists" }) }), playlists.length === 0 ? (_jsx(Text, { color: theme.colors.dim, children: "No playlists yet" })) : (playlists.map((playlist, index) => {
|
|
133
|
-
const isSelected = index === selectedIndex;
|
|
134
|
-
const isRenaming = renamingPlaylistId === playlist.playlistId && isSelected;
|
|
135
|
-
const rowBackground = isSelected ? theme.colors.secondary : undefined;
|
|
136
|
-
return (_jsxs(Box, { paddingX: 1, backgroundColor: rowBackground, children: [_jsxs(Text, { color: isSelected ? theme.colors.background : theme.colors.primary, bold: isSelected, children: [index + 1, "."] }), _jsx(Text, { children: " " }), _jsxs(Box, { flexDirection: "column", children: [isRenaming ? (_jsx(TextInput, { value: renameValue, onChange: setRenameValue, onSubmit: handleRenameSubmit, placeholder: "Playlist name", focus: true })) : (_jsx(Text, { color: isSelected ? theme.colors.background : theme.colors.text, bold: isSelected, children: playlist.name })), _jsx(Text, { color: theme.colors.dim, children: ` (${playlist.tracks.length} tracks)` })] })] }, playlist.playlistId));
|
|
137
|
-
})), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.colors.dim, children: [_jsx(Text, { color: theme.colors.text, children: "Enter" }), " to play |", ' ', _jsx(Text, { color: theme.colors.text, children: "r" }), " rename |", ' ', _jsx(Text, { color: theme.colors.text, children: "c" }), " create |", ' ', _jsx(Text, { color: theme.colors.text, children: "Shift+D" }), " download |", ' ', _jsx(Text, { color: theme.colors.text, children: "D" }), " delete |", ' ', _jsx(Text, { color: theme.colors.text, children: "Esc" }), " back"] }), lastCreated && (_jsxs(Text, { color: theme.colors.accent, children: [" Created ", lastCreated] })), downloadStatus && (_jsx(Text, { color: theme.colors.accent, children: downloadStatus }))] })] }));
|
|
138
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// Plugin install dialog - prompts for plugin name or URL
|
|
3
|
-
import { useState, useCallback } from 'react';
|
|
4
|
-
import { Box, Text } from 'ink';
|
|
5
|
-
import TextInput from 'ink-text-input';
|
|
6
|
-
import { useTheme } from "../../hooks/useTheme.js";
|
|
7
|
-
import { usePlugins } from "../../stores/plugins.store.js";
|
|
8
|
-
import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
9
|
-
import { KEYBINDINGS } from "../../utils/constants.js";
|
|
10
|
-
export default function PluginInstallDialog({ onClose, }) {
|
|
11
|
-
const { theme } = useTheme();
|
|
12
|
-
const { installPlugin, state } = usePlugins();
|
|
13
|
-
const [input, setInput] = useState('');
|
|
14
|
-
const [installing, setInstalling] = useState(false);
|
|
15
|
-
const [result, setResult] = useState(null);
|
|
16
|
-
const handleSubmit = useCallback(async () => {
|
|
17
|
-
if (!input.trim() || installing)
|
|
18
|
-
return;
|
|
19
|
-
setInstalling(true);
|
|
20
|
-
setResult(null);
|
|
21
|
-
const installResult = await installPlugin(input.trim());
|
|
22
|
-
setInstalling(false);
|
|
23
|
-
setResult({
|
|
24
|
-
success: installResult.success,
|
|
25
|
-
message: installResult.success
|
|
26
|
-
? `Successfully installed ${installResult.pluginId}`
|
|
27
|
-
: installResult.error || 'Installation failed',
|
|
28
|
-
});
|
|
29
|
-
if (installResult.success) {
|
|
30
|
-
// Close after a brief delay on success
|
|
31
|
-
setTimeout(onClose, 1500);
|
|
32
|
-
}
|
|
33
|
-
}, [input, installing, installPlugin, onClose]);
|
|
34
|
-
const handleClose = useCallback(() => {
|
|
35
|
-
if (!installing) {
|
|
36
|
-
onClose();
|
|
37
|
-
}
|
|
38
|
-
}, [installing, onClose]);
|
|
39
|
-
useKeyBinding(KEYBINDINGS.BACK, handleClose);
|
|
40
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { borderStyle: "double", borderColor: theme.colors.secondary, paddingX: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Install Plugin" }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.colors.dim, children: "Enter a plugin name (from default repo) or GitHub URL:" }) }), _jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: theme.colors.text, children: '> ' }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit, placeholder: "e.g., adblock or https://github.com/user/plugin" })] }), installing && (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.colors.warning, children: "Installing..." }) })), result && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: result.success ? theme.colors.success : theme.colors.error, children: [result.success ? '✓' : '✗', " ", result.message] }) })), state.error && !result && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.colors.error, children: ["Error: ", state.error] }) })), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: theme.colors.dim, children: ["Press ", _jsx(Text, { color: theme.colors.text, children: "Enter" }), " to install,", ' ', _jsx(Text, { color: theme.colors.text, children: "Esc" }), " to cancel"] }) })] }));
|
|
41
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// Available plugins component - displays plugins from the default repo
|
|
3
|
-
import { useMemo } from 'react';
|
|
4
|
-
import { Box, Text } from 'ink';
|
|
5
|
-
import { useTheme } from "../../hooks/useTheme.js";
|
|
6
|
-
import { usePlugins } from "../../stores/plugins.store.js";
|
|
7
|
-
// Mock available plugins (in production, these would be fetched from the repo)
|
|
8
|
-
const AVAILABLE_PLUGINS = [
|
|
9
|
-
{
|
|
10
|
-
id: 'adblock',
|
|
11
|
-
name: 'Adblock',
|
|
12
|
-
version: '1.0.0',
|
|
13
|
-
description: 'Blocks ads by filtering known ad video IDs',
|
|
14
|
-
author: 'involvex',
|
|
15
|
-
repository: 'https://github.com/involvex/youtube-music-cli-plugins',
|
|
16
|
-
installUrl: 'adblock',
|
|
17
|
-
tags: ['audio', 'filter'],
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
id: 'now-playing',
|
|
21
|
-
name: 'Now Playing',
|
|
22
|
-
version: '1.0.0',
|
|
23
|
-
description: 'Shows system notifications when track changes',
|
|
24
|
-
author: 'involvex',
|
|
25
|
-
repository: 'https://github.com/involvex/youtube-music-cli-plugins',
|
|
26
|
-
installUrl: 'now-playing',
|
|
27
|
-
tags: ['notifications'],
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
id: 'lyrics',
|
|
31
|
-
name: 'Lyrics',
|
|
32
|
-
version: '1.0.0',
|
|
33
|
-
description: 'Displays lyrics for the current track',
|
|
34
|
-
author: 'involvex',
|
|
35
|
-
repository: 'https://github.com/involvex/youtube-music-cli-plugins',
|
|
36
|
-
installUrl: 'lyrics',
|
|
37
|
-
tags: ['ui', 'lyrics'],
|
|
38
|
-
},
|
|
39
|
-
];
|
|
40
|
-
export default function PluginsAvailable({ selectedIndex, }) {
|
|
41
|
-
const { theme } = useTheme();
|
|
42
|
-
const { state } = usePlugins();
|
|
43
|
-
// Use useMemo instead of useEffect to avoid setState in effect
|
|
44
|
-
const plugins = useMemo(() => {
|
|
45
|
-
const installedIds = new Set(state.installedPlugins.map(p => p.manifest.id));
|
|
46
|
-
return AVAILABLE_PLUGINS.filter(p => !installedIds.has(p.id));
|
|
47
|
-
}, [state.installedPlugins]);
|
|
48
|
-
if (plugins.length === 0) {
|
|
49
|
-
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.colors.dim, children: "All available plugins are already installed." }) }));
|
|
50
|
-
}
|
|
51
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { paddingX: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Available Plugins" }) }), plugins.map((plugin, index) => {
|
|
52
|
-
const isSelected = index === selectedIndex;
|
|
53
|
-
return (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: isSelected ? theme.colors.primary : undefined, color: isSelected ? theme.colors.background : theme.colors.text, bold: isSelected, children: [plugin.name, _jsxs(Text, { color: isSelected ? undefined : theme.colors.dim, children: [' ', "v", plugin.version, " - ", plugin.description] })] }) }, plugin.id));
|
|
54
|
-
})] }));
|
|
55
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// Plugins list component - displays installed plugins
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
import { useTheme } from "../../hooks/useTheme.js";
|
|
5
|
-
export default function PluginsList({ plugins, selectedIndex, }) {
|
|
6
|
-
const { theme } = useTheme();
|
|
7
|
-
if (plugins.length === 0) {
|
|
8
|
-
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.colors.dim, children: "No plugins installed. Press 'i' to install a plugin." }) }));
|
|
9
|
-
}
|
|
10
|
-
return (_jsx(Box, { flexDirection: "column", children: plugins.map((plugin, index) => {
|
|
11
|
-
const isSelected = index === selectedIndex;
|
|
12
|
-
const statusIcon = plugin.enabled ? '●' : '○';
|
|
13
|
-
const statusColor = plugin.enabled
|
|
14
|
-
? theme.colors.success
|
|
15
|
-
: theme.colors.dim;
|
|
16
|
-
return (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: isSelected ? theme.colors.primary : undefined, color: isSelected ? theme.colors.background : theme.colors.text, bold: isSelected, children: [_jsx(Text, { color: isSelected ? undefined : statusColor, children: statusIcon }), ' ', plugin.manifest.name, _jsxs(Text, { color: isSelected ? undefined : theme.colors.dim, children: [' ', "v", plugin.manifest.version] })] }) }, plugin.manifest.id));
|
|
17
|
-
}) }));
|
|
18
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// Search bar component
|
|
3
|
-
import { useNavigation } from "../../hooks/useNavigation.js";
|
|
4
|
-
import { useState, useCallback } from 'react';
|
|
5
|
-
import React from 'react';
|
|
6
|
-
import { SEARCH_TYPE } from "../../utils/constants.js";
|
|
7
|
-
import { useTheme } from "../../hooks/useTheme.js";
|
|
8
|
-
import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
9
|
-
import { useKeyboardBlocker } from "../../hooks/useKeyboardBlocker.js";
|
|
10
|
-
import { Box, Text } from 'ink';
|
|
11
|
-
import TextInput from 'ink-text-input';
|
|
12
|
-
import { getConfigService } from "../../services/config/config.service.js";
|
|
13
|
-
function SearchBar({ onInput, isActive = true }) {
|
|
14
|
-
const { theme } = useTheme();
|
|
15
|
-
const { state: navState, dispatch } = useNavigation();
|
|
16
|
-
const [input, setInput] = useState('');
|
|
17
|
-
const config = getConfigService();
|
|
18
|
-
const searchTypes = Object.values(SEARCH_TYPE);
|
|
19
|
-
// Handle type switching
|
|
20
|
-
const cycleType = useCallback(() => {
|
|
21
|
-
if (!isActive)
|
|
22
|
-
return;
|
|
23
|
-
const currentIndex = searchTypes.indexOf(navState.searchType);
|
|
24
|
-
const nextIndex = (currentIndex + 1) % searchTypes.length;
|
|
25
|
-
const nextType = searchTypes[nextIndex];
|
|
26
|
-
if (nextType) {
|
|
27
|
-
dispatch({
|
|
28
|
-
category: 'SET_SEARCH_CATEGORY',
|
|
29
|
-
searchType: nextType,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
}, [navState.searchType, searchTypes, dispatch, isActive]);
|
|
33
|
-
// Handle submit via ink-text-input's onSubmit
|
|
34
|
-
const handleSubmit = useCallback((value) => {
|
|
35
|
-
if (value && isActive) {
|
|
36
|
-
config.addToSearchHistory(value);
|
|
37
|
-
dispatch({ category: 'SET_SEARCH_QUERY', query: value });
|
|
38
|
-
onInput(value);
|
|
39
|
-
}
|
|
40
|
-
}, [dispatch, onInput, isActive, config]);
|
|
41
|
-
// Handle clearing search
|
|
42
|
-
const clearSearch = useCallback(() => {
|
|
43
|
-
if (isActive) {
|
|
44
|
-
setInput('');
|
|
45
|
-
onInput('');
|
|
46
|
-
}
|
|
47
|
-
}, [isActive, onInput]);
|
|
48
|
-
useKeyBinding(['tab'], cycleType);
|
|
49
|
-
useKeyBinding(['escape'], clearSearch, { bypassBlock: true });
|
|
50
|
-
useKeyboardBlocker(isActive);
|
|
51
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.secondary, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.dim, children: "Type: " }), searchTypes.map((type, index) => (_jsxs(Text, { color: navState.searchType === type
|
|
52
|
-
? theme.colors.primary
|
|
53
|
-
: theme.colors.dim, bold: navState.searchType === type, children: [type, index < searchTypes.length - 1 && ' '] }, type))), _jsx(Text, { color: theme.colors.dim, children: " (Tab to switch)" })] }), isActive && (_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.primary, children: "Search: " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit, placeholder: "Type to search...", focus: isActive })] })), !isActive && (_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.primary, children: "Search: " }), _jsx(Text, { color: theme.colors.dim, children: input || 'Type to search...' })] }))] }));
|
|
54
|
-
}
|
|
55
|
-
export default React.memo(SearchBar);
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// Search history component
|
|
3
|
-
import { useState, useCallback } from 'react';
|
|
4
|
-
import { Box, Text } from 'ink';
|
|
5
|
-
import { useTheme } from "../../hooks/useTheme.js";
|
|
6
|
-
import { useNavigation } from "../../hooks/useNavigation.js";
|
|
7
|
-
import { getConfigService } from "../../services/config/config.service.js";
|
|
8
|
-
import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
9
|
-
import { KEYBINDINGS, VIEW } from "../../utils/constants.js";
|
|
10
|
-
export default function SearchHistory({ onSelect }) {
|
|
11
|
-
const { theme } = useTheme();
|
|
12
|
-
const { dispatch } = useNavigation();
|
|
13
|
-
const config = getConfigService();
|
|
14
|
-
const history = config.getSearchHistory();
|
|
15
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
16
|
-
const navigateUp = useCallback(() => {
|
|
17
|
-
setSelectedIndex(prev => Math.max(0, prev - 1));
|
|
18
|
-
}, []);
|
|
19
|
-
const navigateDown = useCallback(() => {
|
|
20
|
-
setSelectedIndex(prev => Math.min(history.length - 1, prev + 1));
|
|
21
|
-
}, [history.length]);
|
|
22
|
-
const handleSelect = useCallback(() => {
|
|
23
|
-
const query = history[selectedIndex];
|
|
24
|
-
if (query) {
|
|
25
|
-
dispatch({ category: 'NAVIGATE', view: VIEW.SEARCH });
|
|
26
|
-
onSelect(query);
|
|
27
|
-
}
|
|
28
|
-
}, [history, selectedIndex, dispatch, onSelect]);
|
|
29
|
-
useKeyBinding(KEYBINDINGS.UP, navigateUp);
|
|
30
|
-
useKeyBinding(KEYBINDINGS.DOWN, navigateDown);
|
|
31
|
-
useKeyBinding(KEYBINDINGS.SELECT, handleSelect);
|
|
32
|
-
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: "Search History" }) }), history.length === 0 ? (_jsx(Text, { color: theme.colors.dim, children: "No search history yet" })) : (history.map((query, index) => (_jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === index ? theme.colors.primary : undefined, color: selectedIndex === index
|
|
33
|
-
? theme.colors.background
|
|
34
|
-
: theme.colors.text, children: query }) }, index)))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.dim, children: "\u2191\u2193 to navigate \u2022 Enter to search \u2022 Esc to go back" }) })] }));
|
|
35
|
-
}
|