@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.
- package/CHANGELOG.md +4 -0
- package/dist/cli.js.map +1004 -0
- package/dist/source/hooks/usePlayer.d.ts +1 -0
- package/dist/source/services/player-state/player-state.service.d.ts +1 -0
- package/dist/source/stores/player.store.d.ts +1 -0
- package/dist/source/types/actions.d.ts +4 -0
- package/dist/source/types/player.types.d.ts +3 -2
- package/dist/source/utils/constants.d.ts +1 -0
- package/dist/source/utils/icons.d.ts +1 -0
- 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 -80
- 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 -122
- 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 -506
- package/dist/source/services/web/web-streaming.service.js +0 -290
- 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 -724
- 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 -134
- package/dist/source/utils/format.js +0 -24
- package/dist/source/utils/icons.js +0 -26
- 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
|
-
}
|