@involvex/youtube-music-cli 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +352 -0
- package/dist/eslint.config.d.ts +2 -0
- package/dist/eslint.config.js +55 -0
- package/dist/source/app.d.ts +4 -0
- package/dist/source/app.js +17 -0
- package/dist/source/cli.d.ts +2 -0
- package/dist/source/cli.js +241 -0
- package/dist/source/components/common/ErrorBoundary.d.ts +15 -0
- package/dist/source/components/common/ErrorBoundary.js +22 -0
- package/dist/source/components/common/Help.d.ts +1 -0
- package/dist/source/components/common/Help.js +10 -0
- package/dist/source/components/common/ShortcutsBar.d.ts +1 -0
- package/dist/source/components/common/ShortcutsBar.js +33 -0
- package/dist/source/components/config/ConfigLayout.d.ts +1 -0
- package/dist/source/components/config/ConfigLayout.js +84 -0
- package/dist/source/components/layouts/MainLayout.d.ts +4 -0
- package/dist/source/components/layouts/MainLayout.js +83 -0
- package/dist/source/components/layouts/PlayerLayout.d.ts +1 -0
- package/dist/source/components/layouts/PlayerLayout.js +10 -0
- package/dist/source/components/layouts/PluginsLayout.d.ts +1 -0
- package/dist/source/components/layouts/PluginsLayout.js +77 -0
- package/dist/source/components/layouts/SearchLayout.d.ts +4 -0
- package/dist/source/components/layouts/SearchLayout.js +81 -0
- package/dist/source/components/player/NowPlaying.d.ts +1 -0
- package/dist/source/components/player/NowPlaying.js +21 -0
- package/dist/source/components/player/PlayerControls.d.ts +1 -0
- package/dist/source/components/player/PlayerControls.js +41 -0
- package/dist/source/components/player/ProgressBar.d.ts +1 -0
- package/dist/source/components/player/ProgressBar.js +18 -0
- package/dist/source/components/player/QueueList.d.ts +4 -0
- package/dist/source/components/player/QueueList.js +30 -0
- package/dist/source/components/player/Suggestions.d.ts +1 -0
- package/dist/source/components/player/Suggestions.js +47 -0
- package/dist/source/components/playlist/PlaylistList.d.ts +1 -0
- package/dist/source/components/playlist/PlaylistList.js +11 -0
- package/dist/source/components/plugins/PluginInstallDialog.d.ts +5 -0
- package/dist/source/components/plugins/PluginInstallDialog.js +41 -0
- package/dist/source/components/plugins/PluginsAvailable.d.ts +5 -0
- package/dist/source/components/plugins/PluginsAvailable.js +55 -0
- package/dist/source/components/plugins/PluginsList.d.ts +8 -0
- package/dist/source/components/plugins/PluginsList.js +18 -0
- package/dist/source/components/search/SearchBar.d.ts +8 -0
- package/dist/source/components/search/SearchBar.js +50 -0
- package/dist/source/components/search/SearchResults.d.ts +10 -0
- package/dist/source/components/search/SearchResults.js +111 -0
- package/dist/source/components/settings/Settings.d.ts +1 -0
- package/dist/source/components/settings/Settings.js +42 -0
- package/dist/source/components/theme/ThemeSwitcher.d.ts +1 -0
- package/dist/source/components/theme/ThemeSwitcher.js +11 -0
- package/dist/source/config/themes.config.d.ts +3 -0
- package/dist/source/config/themes.config.js +63 -0
- package/dist/source/contexts/theme.context.d.ts +13 -0
- package/dist/source/contexts/theme.context.js +29 -0
- package/dist/source/hooks/useKeyboard.d.ts +10 -0
- package/dist/source/hooks/useKeyboard.js +104 -0
- package/dist/source/hooks/useNavigation.d.ts +1 -0
- package/dist/source/hooks/useNavigation.js +5 -0
- package/dist/source/hooks/usePlayer.d.ts +23 -0
- package/dist/source/hooks/usePlayer.js +35 -0
- package/dist/source/hooks/usePlaylist.d.ts +8 -0
- package/dist/source/hooks/usePlaylist.js +50 -0
- package/dist/source/hooks/useSearch.d.ts +8 -0
- package/dist/source/hooks/useSearch.js +76 -0
- package/dist/source/hooks/useTerminalSize.d.ts +4 -0
- package/dist/source/hooks/useTerminalSize.js +24 -0
- package/dist/source/hooks/useTheme.d.ts +6 -0
- package/dist/source/hooks/useTheme.js +5 -0
- package/dist/source/hooks/useYouTubeMusic.d.ts +11 -0
- package/dist/source/hooks/useYouTubeMusic.js +112 -0
- package/dist/source/main.d.ts +4 -0
- package/dist/source/main.js +69 -0
- package/dist/source/services/config/config.service.d.ts +26 -0
- package/dist/source/services/config/config.service.js +125 -0
- package/dist/source/services/logger/logger.service.d.ts +10 -0
- package/dist/source/services/logger/logger.service.js +52 -0
- package/dist/source/services/player/player.service.d.ts +58 -0
- package/dist/source/services/player/player.service.js +349 -0
- package/dist/source/services/player-state/player-state.service.d.ts +24 -0
- package/dist/source/services/player-state/player-state.service.js +122 -0
- package/dist/source/services/plugin/plugin-audio-api.d.ts +17 -0
- package/dist/source/services/plugin/plugin-audio-api.js +36 -0
- package/dist/source/services/plugin/plugin-context.d.ts +5 -0
- package/dist/source/services/plugin/plugin-context.js +256 -0
- package/dist/source/services/plugin/plugin-hooks.service.d.ts +62 -0
- package/dist/source/services/plugin/plugin-hooks.service.js +135 -0
- package/dist/source/services/plugin/plugin-installer.service.d.ts +27 -0
- package/dist/source/services/plugin/plugin-installer.service.js +247 -0
- package/dist/source/services/plugin/plugin-loader.service.d.ts +33 -0
- package/dist/source/services/plugin/plugin-loader.service.js +161 -0
- package/dist/source/services/plugin/plugin-permissions.service.d.ts +72 -0
- package/dist/source/services/plugin/plugin-permissions.service.js +194 -0
- package/dist/source/services/plugin/plugin-registry.service.d.ts +76 -0
- package/dist/source/services/plugin/plugin-registry.service.js +215 -0
- package/dist/source/services/plugin/plugin-ui-api.d.ts +25 -0
- package/dist/source/services/plugin/plugin-ui-api.js +46 -0
- package/dist/source/services/plugin/plugin-updater.service.d.ts +23 -0
- package/dist/source/services/plugin/plugin-updater.service.js +206 -0
- package/dist/source/services/youtube-music/api.d.ts +13 -0
- package/dist/source/services/youtube-music/api.js +371 -0
- package/dist/source/services/youtube-music/search.service.d.ts +11 -0
- package/dist/source/services/youtube-music/search.service.js +38 -0
- package/dist/source/stores/navigation.store.d.ts +10 -0
- package/dist/source/stores/navigation.store.js +67 -0
- package/dist/source/stores/player.store.d.ts +28 -0
- package/dist/source/stores/player.store.js +458 -0
- package/dist/source/stores/plugins.store.d.ts +46 -0
- package/dist/source/stores/plugins.store.js +177 -0
- package/dist/source/types/actions.d.ts +119 -0
- package/dist/source/types/actions.js +1 -0
- package/dist/source/types/cli.types.d.ts +14 -0
- package/dist/source/types/cli.types.js +1 -0
- package/dist/source/types/config.types.d.ts +19 -0
- package/dist/source/types/config.types.js +1 -0
- package/dist/source/types/keyboard.types.d.ts +5 -0
- package/dist/source/types/keyboard.types.js +1 -0
- package/dist/source/types/navigation.types.d.ts +14 -0
- package/dist/source/types/navigation.types.js +1 -0
- package/dist/source/types/player.types.d.ts +16 -0
- package/dist/source/types/player.types.js +1 -0
- package/dist/source/types/playlist.types.d.ts +12 -0
- package/dist/source/types/playlist.types.js +1 -0
- package/dist/source/types/plugin.types.d.ts +239 -0
- package/dist/source/types/plugin.types.js +1 -0
- package/dist/source/types/theme.types.d.ts +18 -0
- package/dist/source/types/theme.types.js +1 -0
- package/dist/source/types/youtube-music.types.d.ts +35 -0
- package/dist/source/types/youtube-music.types.js +1 -0
- package/dist/source/types/youtubei.types.d.ts +60 -0
- package/dist/source/types/youtubei.types.js +3 -0
- package/dist/source/utils/constants.d.ts +65 -0
- package/dist/source/utils/constants.js +82 -0
- package/dist/source/utils/format.d.ts +3 -0
- package/dist/source/utils/format.js +24 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +13 -0
- package/package.json +100 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Error boundary component for robust error handling
|
|
3
|
+
import { Component } from 'react';
|
|
4
|
+
import { Box, Text } from 'ink';
|
|
5
|
+
export class ErrorBoundary extends Component {
|
|
6
|
+
state = {
|
|
7
|
+
hasError: false,
|
|
8
|
+
error: null,
|
|
9
|
+
};
|
|
10
|
+
static getDerivedStateFromError(error) {
|
|
11
|
+
return { hasError: true, error };
|
|
12
|
+
}
|
|
13
|
+
componentDidCatch(error, errorInfo) {
|
|
14
|
+
console.error('Uncaught error:', error, errorInfo);
|
|
15
|
+
}
|
|
16
|
+
render() {
|
|
17
|
+
if (this.state.hasError) {
|
|
18
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "red", children: [_jsx(Text, { color: "red", bold: true, children: "Something went wrong!" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "white", children: this.state.error?.message }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "dim", children: "Press Ctrl+C to exit and restart the CLI." }) })] }));
|
|
19
|
+
}
|
|
20
|
+
return this.props.children;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function Help(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Help component for keyboard shortcuts
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { useTheme } from "../../hooks/useTheme.js";
|
|
5
|
+
import { useNavigation } from "../../hooks/useNavigation.js";
|
|
6
|
+
export default function Help() {
|
|
7
|
+
const { theme } = useTheme();
|
|
8
|
+
const { dispatch: _dispatch } = useNavigation();
|
|
9
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, padding: 1, children: [_jsx(Box, { borderStyle: "single", borderColor: theme.colors.secondary, paddingX: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Keyboard Shortcuts" }) }), _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Global" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "q / Esc" }), " - Quit", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "?" }), " - Help", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "/" }), " - Search", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "p" }), " - Playlists", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "g" }), " - Suggestions", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "," }), " - Settings"] }) }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Player" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "Space" }), " - Play/Pause", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "n" }), " - Next", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "b" }), " - Previous", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "=" }), " - Volume Up", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "-" }), " - Volume Down", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "s" }), " - Toggle Shuffle", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "r" }), " - Toggle Repeat"] }) }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Navigation" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "Up" }), " /", _jsx(Text, { children: " " }), _jsx(Text, { color: theme.colors.text, children: "k" }), " - Move Up", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Down" }), " /", _jsx(Text, { children: " " }), _jsx(Text, { color: theme.colors.text, children: "j" }), " - Move Down", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Enter" }), " - Select", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Esc" }), " - Go Back"] }) }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Search" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "Tab" }), " - Switch Search Type", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Esc" }), " - Clear Search", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "[ / ]" }), " - Results Limit"] }) }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Playlist" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "a" }), " - Add to Playlist", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "d" }), " - Remove from Playlist", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "c" }), " - Create Playlist", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "D" }), " - Delete Playlist"] }) }), _jsxs(Text, { color: theme.colors.dim, children: ["Press ", _jsx(Text, { color: theme.colors.text, children: "Esc" }), " or", ' ', _jsx(Text, { color: theme.colors.text, children: "?" }), " to close"] })] })] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function ShortcutsBar(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Shortcuts bar component
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { useCallback } from 'react';
|
|
5
|
+
import { usePlayer } from "../../hooks/usePlayer.js";
|
|
6
|
+
import { useNavigation } from "../../hooks/useNavigation.js";
|
|
7
|
+
import { useTheme } from "../../hooks/useTheme.js";
|
|
8
|
+
import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
9
|
+
import { KEYBINDINGS } from "../../utils/constants.js";
|
|
10
|
+
export default function ShortcutsBar() {
|
|
11
|
+
const { theme } = useTheme();
|
|
12
|
+
const { dispatch: navDispatch } = useNavigation();
|
|
13
|
+
const { state: playerState, pause, resume, next, previous, volumeUp, volumeDown, } = usePlayer();
|
|
14
|
+
// Register key bindings globally
|
|
15
|
+
const handlePlayPause = () => {
|
|
16
|
+
if (playerState.isPlaying) {
|
|
17
|
+
pause();
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
resume();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const goConfig = useCallback(() => {
|
|
24
|
+
navDispatch({ category: 'NAVIGATE', view: 'config' });
|
|
25
|
+
}, [navDispatch]);
|
|
26
|
+
useKeyBinding(KEYBINDINGS.PLAY_PAUSE, handlePlayPause);
|
|
27
|
+
useKeyBinding(KEYBINDINGS.NEXT, next);
|
|
28
|
+
useKeyBinding(KEYBINDINGS.PREVIOUS, previous);
|
|
29
|
+
useKeyBinding(KEYBINDINGS.VOLUME_UP, volumeUp);
|
|
30
|
+
useKeyBinding(KEYBINDINGS.VOLUME_DOWN, volumeDown);
|
|
31
|
+
useKeyBinding(KEYBINDINGS.SETTINGS, goConfig);
|
|
32
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: theme.colors.dim, paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: theme.colors.dim, children: ["Shortcuts: ", _jsx(Text, { color: theme.colors.text, children: "Space" }), " Play/Pause |", ' ', _jsx(Text, { color: theme.colors.text, children: "n" }), " Next |", ' ', _jsx(Text, { color: theme.colors.text, children: "p" }), " Previous |", ' ', _jsx(Text, { color: theme.colors.text, children: "/" }), " Search |", ' ', _jsx(Text, { color: theme.colors.text, children: "," }), " Settings |", ' ', _jsx(Text, { color: theme.colors.text, children: "?" }), " Help |", ' ', _jsx(Text, { color: theme.colors.text, children: "q" }), " Quit"] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.dim, children: "[=/" }), "-", _jsx(Text, { color: theme.colors.dim, children: "]" }), " Vol:", ' ', _jsxs(Text, { color: theme.colors.primary, children: [playerState.volume, "%"] })] })] }));
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function ConfigLayout(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Config screen layout
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { useState, useCallback } from 'react';
|
|
5
|
+
import { useTheme } from "../../hooks/useTheme.js";
|
|
6
|
+
import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
7
|
+
import { useNavigation } from "../../hooks/useNavigation.js";
|
|
8
|
+
import { KEYBINDINGS } from "../../utils/constants.js";
|
|
9
|
+
import { getConfigService } from "../../services/config/config.service.js";
|
|
10
|
+
export default function ConfigLayout() {
|
|
11
|
+
const { theme, setTheme } = useTheme();
|
|
12
|
+
const { dispatch } = useNavigation();
|
|
13
|
+
const [selectedSection, setSelectedSection] = useState('theme');
|
|
14
|
+
const config = getConfigService();
|
|
15
|
+
// Navigate sections
|
|
16
|
+
const goUp = useCallback(() => {
|
|
17
|
+
const sections = ['theme', 'quality', 'volumeStep'];
|
|
18
|
+
const currentIndex = sections.indexOf(selectedSection);
|
|
19
|
+
if (currentIndex > 0) {
|
|
20
|
+
setSelectedSection(sections[currentIndex - 1]);
|
|
21
|
+
}
|
|
22
|
+
}, [selectedSection]);
|
|
23
|
+
const goDown = useCallback(() => {
|
|
24
|
+
const sections = ['theme', 'quality', 'volumeStep'];
|
|
25
|
+
const currentIndex = sections.indexOf(selectedSection);
|
|
26
|
+
if (currentIndex < sections.length - 1) {
|
|
27
|
+
setSelectedSection(sections[currentIndex + 1]);
|
|
28
|
+
}
|
|
29
|
+
}, [selectedSection]);
|
|
30
|
+
// Handle Enter key based on selected section
|
|
31
|
+
const handleSelect = useCallback(() => {
|
|
32
|
+
if (selectedSection === 'theme') {
|
|
33
|
+
const themes = ['dark', 'light', 'midnight', 'matrix'];
|
|
34
|
+
const currentTheme = theme.name;
|
|
35
|
+
const currentIndex = themes.indexOf(currentTheme);
|
|
36
|
+
const nextIndex = (currentIndex + 1) % themes.length;
|
|
37
|
+
const nextTheme = themes[nextIndex];
|
|
38
|
+
setTheme(nextTheme);
|
|
39
|
+
config.set('theme', nextTheme);
|
|
40
|
+
}
|
|
41
|
+
else if (selectedSection === 'quality') {
|
|
42
|
+
const qualities = ['low', 'medium', 'high'];
|
|
43
|
+
const currentQuality = config.get('streamQuality');
|
|
44
|
+
const currentIndex = qualities.indexOf(currentQuality);
|
|
45
|
+
const nextIndex = (currentIndex + 1) % qualities.length;
|
|
46
|
+
config.set('streamQuality', qualities[nextIndex]);
|
|
47
|
+
}
|
|
48
|
+
}, [selectedSection, config, theme, setTheme]);
|
|
49
|
+
// Change volume step
|
|
50
|
+
const increaseVolumeStep = useCallback(() => {
|
|
51
|
+
if (selectedSection === 'volumeStep') {
|
|
52
|
+
const current = config.get('volume');
|
|
53
|
+
if (current < 100) {
|
|
54
|
+
config.set('volume', Math.min(100, current + 10));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}, [selectedSection, config]);
|
|
58
|
+
const decreaseVolumeStep = useCallback(() => {
|
|
59
|
+
if (selectedSection === 'volumeStep') {
|
|
60
|
+
const current = config.get('volume');
|
|
61
|
+
if (current > 0) {
|
|
62
|
+
config.set('volume', Math.max(0, current - 10));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}, [selectedSection, config]);
|
|
66
|
+
// Go back
|
|
67
|
+
const goBack = useCallback(() => {
|
|
68
|
+
dispatch({ category: 'GO_BACK' });
|
|
69
|
+
}, [dispatch]);
|
|
70
|
+
useKeyBinding(KEYBINDINGS.UP, goUp);
|
|
71
|
+
useKeyBinding(KEYBINDINGS.DOWN, goDown);
|
|
72
|
+
useKeyBinding(KEYBINDINGS.SELECT, handleSelect);
|
|
73
|
+
useKeyBinding(KEYBINDINGS.VOLUME_UP, increaseVolumeStep);
|
|
74
|
+
useKeyBinding(KEYBINDINGS.VOLUME_DOWN, decreaseVolumeStep);
|
|
75
|
+
useKeyBinding(KEYBINDINGS.BACK, goBack);
|
|
76
|
+
const currentTheme = theme.name;
|
|
77
|
+
const currentQuality = config.get('streamQuality') || 'high';
|
|
78
|
+
const currentVolume = config.get('volume') || 70;
|
|
79
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { borderStyle: "single", borderColor: theme.colors.secondary, paddingX: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Settings" }) }), _jsxs(Box, { paddingX: 1, borderStyle: "single", borderColor: selectedSection === 'theme' ? theme.colors.primary : theme.colors.dim, children: [_jsxs(Text, { color: theme.colors.text, children: ["Theme: ", _jsx(Text, { color: theme.colors.primary, children: currentTheme })] }), selectedSection === 'theme' && (_jsx(Text, { color: theme.colors.dim, children: " (Press Enter to cycle)" }))] }), _jsxs(Box, { paddingX: 1, borderStyle: "single", borderColor: selectedSection === 'quality'
|
|
80
|
+
? theme.colors.primary
|
|
81
|
+
: theme.colors.dim, children: [_jsxs(Text, { color: theme.colors.text, children: ["Stream Quality:", ' ', _jsx(Text, { color: theme.colors.primary, children: currentQuality })] }), selectedSection === 'quality' && (_jsx(Text, { color: theme.colors.dim, children: " (Press Enter to cycle)" }))] }), _jsxs(Box, { paddingX: 1, borderStyle: "single", borderColor: selectedSection === 'volumeStep'
|
|
82
|
+
? theme.colors.primary
|
|
83
|
+
: theme.colors.dim, children: [_jsxs(Text, { color: theme.colors.text, children: ["Default Volume:", ' ', _jsxs(Text, { color: theme.colors.primary, children: [currentVolume, "%"] })] }), selectedSection === 'volumeStep' && (_jsx(Text, { color: theme.colors.dim, children: " (Press =/- to adjust)" }))] })] }));
|
|
84
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Main layout shell
|
|
3
|
+
import { useCallback, useMemo } from 'react';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { useNavigation } from "../../hooks/useNavigation.js";
|
|
6
|
+
import PlaylistList from "../playlist/PlaylistList.js";
|
|
7
|
+
import Help from "../common/Help.js";
|
|
8
|
+
import { useTheme } from "../../hooks/useTheme.js";
|
|
9
|
+
import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
10
|
+
import SearchLayout from "./SearchLayout.js";
|
|
11
|
+
import PlayerLayout from "./PlayerLayout.js";
|
|
12
|
+
import PluginsLayout from "./PluginsLayout.js";
|
|
13
|
+
import Suggestions from "../player/Suggestions.js";
|
|
14
|
+
import Settings from "../settings/Settings.js";
|
|
15
|
+
import ConfigLayout from "../config/ConfigLayout.js";
|
|
16
|
+
import ShortcutsBar from "../common/ShortcutsBar.js";
|
|
17
|
+
import { KEYBINDINGS, VIEW } from "../../utils/constants.js";
|
|
18
|
+
import { Box } from 'ink';
|
|
19
|
+
import { useTerminalSize } from "../../hooks/useTerminalSize.js";
|
|
20
|
+
function MainLayout() {
|
|
21
|
+
const { theme } = useTheme();
|
|
22
|
+
const { state: navState, dispatch } = useNavigation();
|
|
23
|
+
const { columns } = useTerminalSize();
|
|
24
|
+
// Responsive padding based on terminal size
|
|
25
|
+
const getPadding = () => (columns < 100 ? 0 : 1);
|
|
26
|
+
// Navigate to different views
|
|
27
|
+
const goToSearch = useCallback(() => {
|
|
28
|
+
dispatch({ category: 'NAVIGATE', view: VIEW.SEARCH });
|
|
29
|
+
}, [dispatch]);
|
|
30
|
+
const goToPlaylists = useCallback(() => {
|
|
31
|
+
dispatch({ category: 'NAVIGATE', view: VIEW.PLAYLISTS });
|
|
32
|
+
}, [dispatch]);
|
|
33
|
+
const goToSuggestions = useCallback(() => {
|
|
34
|
+
dispatch({ category: 'NAVIGATE', view: VIEW.SUGGESTIONS });
|
|
35
|
+
}, [dispatch]);
|
|
36
|
+
const goToSettings = useCallback(() => {
|
|
37
|
+
dispatch({ category: 'NAVIGATE', view: VIEW.SETTINGS });
|
|
38
|
+
}, [dispatch]);
|
|
39
|
+
const goToHelp = useCallback(() => {
|
|
40
|
+
dispatch({ category: 'NAVIGATE', view: VIEW.HELP });
|
|
41
|
+
}, [dispatch]);
|
|
42
|
+
const handleQuit = useCallback(() => {
|
|
43
|
+
// From player view, quit the app
|
|
44
|
+
if (navState.currentView === VIEW.PLAYER) {
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
// From other views, go back
|
|
48
|
+
dispatch({ category: 'GO_BACK' });
|
|
49
|
+
}, [navState.currentView, dispatch]);
|
|
50
|
+
// Global keyboard bindings
|
|
51
|
+
useKeyBinding(KEYBINDINGS.QUIT, handleQuit);
|
|
52
|
+
useKeyBinding(KEYBINDINGS.SEARCH, goToSearch);
|
|
53
|
+
useKeyBinding(KEYBINDINGS.PLAYLISTS, goToPlaylists);
|
|
54
|
+
useKeyBinding(KEYBINDINGS.SUGGESTIONS, goToSuggestions);
|
|
55
|
+
useKeyBinding(KEYBINDINGS.SETTINGS, goToSettings);
|
|
56
|
+
useKeyBinding(KEYBINDINGS.HELP, goToHelp);
|
|
57
|
+
// Memoize the view component to prevent unnecessary remounts
|
|
58
|
+
// Only recreate when currentView actually changes
|
|
59
|
+
const currentView = useMemo(() => {
|
|
60
|
+
switch (navState.currentView) {
|
|
61
|
+
case 'player':
|
|
62
|
+
return _jsx(PlayerLayout, {}, "player");
|
|
63
|
+
case 'search':
|
|
64
|
+
return _jsx(SearchLayout, {}, "search");
|
|
65
|
+
case 'playlists':
|
|
66
|
+
return _jsx(PlaylistList, {}, "playlists");
|
|
67
|
+
case 'suggestions':
|
|
68
|
+
return _jsx(Suggestions, {}, "suggestions");
|
|
69
|
+
case 'settings':
|
|
70
|
+
return _jsx(Settings, {}, "settings");
|
|
71
|
+
case 'plugins':
|
|
72
|
+
return _jsx(PluginsLayout, {}, "plugins");
|
|
73
|
+
case 'config':
|
|
74
|
+
return _jsx(ConfigLayout, {}, "config");
|
|
75
|
+
case 'help':
|
|
76
|
+
return _jsx(Help, {}, "help");
|
|
77
|
+
default:
|
|
78
|
+
return _jsx(PlayerLayout, {}, "player-default");
|
|
79
|
+
}
|
|
80
|
+
}, [navState.currentView]);
|
|
81
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: getPadding(), borderStyle: "single", borderColor: theme.colors.primary, children: [currentView, _jsx(ShortcutsBar, {})] }));
|
|
82
|
+
}
|
|
83
|
+
export default React.memo(MainLayout);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function PlayerLayout(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { usePlayer } from "../../hooks/usePlayer.js";
|
|
3
|
+
import NowPlaying from "../player/NowPlaying.js";
|
|
4
|
+
import ProgressBar from "../player/ProgressBar.js";
|
|
5
|
+
import QueueList from "../player/QueueList.js";
|
|
6
|
+
import { Box } from 'ink';
|
|
7
|
+
export default function PlayerLayout() {
|
|
8
|
+
const { state: playerState } = usePlayer();
|
|
9
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(NowPlaying, {}), _jsx(ProgressBar, {}), playerState.queue.length > 0 && _jsx(QueueList, {})] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function PluginsLayout(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Plugins layout - main plugin management view
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
|
+
import { Box, Text } from 'ink';
|
|
5
|
+
import { useTheme } from "../../hooks/useTheme.js";
|
|
6
|
+
import { usePlugins } from "../../stores/plugins.store.js";
|
|
7
|
+
import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
8
|
+
import { KEYBINDINGS } from "../../utils/constants.js";
|
|
9
|
+
import PluginsList from "../plugins/PluginsList.js";
|
|
10
|
+
import PluginInstallDialog from "../plugins/PluginInstallDialog.js";
|
|
11
|
+
export default function PluginsLayout() {
|
|
12
|
+
const { theme } = useTheme();
|
|
13
|
+
const { state, dispatch, enablePlugin, disablePlugin, uninstallPlugin, updatePlugin, } = usePlugins();
|
|
14
|
+
const [viewMode, setViewMode] = useState('list');
|
|
15
|
+
const { installedPlugins, selectedIndex, isLoading, error, lastAction } = state;
|
|
16
|
+
// Navigation
|
|
17
|
+
const navigateUp = useCallback(() => {
|
|
18
|
+
if (viewMode === 'list') {
|
|
19
|
+
dispatch({
|
|
20
|
+
type: 'SET_SELECTED',
|
|
21
|
+
index: Math.max(0, selectedIndex - 1),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}, [viewMode, selectedIndex, dispatch]);
|
|
25
|
+
const navigateDown = useCallback(() => {
|
|
26
|
+
if (viewMode === 'list') {
|
|
27
|
+
dispatch({
|
|
28
|
+
type: 'SET_SELECTED',
|
|
29
|
+
index: Math.min(installedPlugins.length - 1, selectedIndex + 1),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}, [viewMode, selectedIndex, installedPlugins.length, dispatch]);
|
|
33
|
+
// Actions
|
|
34
|
+
const togglePlugin = useCallback(async () => {
|
|
35
|
+
const plugin = installedPlugins[selectedIndex];
|
|
36
|
+
if (!plugin)
|
|
37
|
+
return;
|
|
38
|
+
if (plugin.enabled) {
|
|
39
|
+
await disablePlugin(plugin.manifest.id);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
await enablePlugin(plugin.manifest.id);
|
|
43
|
+
}
|
|
44
|
+
}, [installedPlugins, selectedIndex, enablePlugin, disablePlugin]);
|
|
45
|
+
const removePlugin = useCallback(async () => {
|
|
46
|
+
const plugin = installedPlugins[selectedIndex];
|
|
47
|
+
if (!plugin)
|
|
48
|
+
return;
|
|
49
|
+
await uninstallPlugin(plugin.manifest.id);
|
|
50
|
+
}, [installedPlugins, selectedIndex, uninstallPlugin]);
|
|
51
|
+
const handleUpdate = useCallback(async () => {
|
|
52
|
+
const plugin = installedPlugins[selectedIndex];
|
|
53
|
+
if (!plugin)
|
|
54
|
+
return;
|
|
55
|
+
await updatePlugin(plugin.manifest.id);
|
|
56
|
+
}, [installedPlugins, selectedIndex, updatePlugin]);
|
|
57
|
+
const openInstall = useCallback(() => {
|
|
58
|
+
setViewMode('install');
|
|
59
|
+
}, []);
|
|
60
|
+
const closeInstall = useCallback(() => {
|
|
61
|
+
setViewMode('list');
|
|
62
|
+
}, []);
|
|
63
|
+
// Key bindings
|
|
64
|
+
useKeyBinding(KEYBINDINGS.UP, navigateUp);
|
|
65
|
+
useKeyBinding(KEYBINDINGS.DOWN, navigateDown);
|
|
66
|
+
useKeyBinding(['e'], togglePlugin);
|
|
67
|
+
useKeyBinding(['r'], removePlugin);
|
|
68
|
+
useKeyBinding(['u'], handleUpdate);
|
|
69
|
+
useKeyBinding(['i'], openInstall);
|
|
70
|
+
// Show install dialog
|
|
71
|
+
if (viewMode === 'install') {
|
|
72
|
+
return _jsx(PluginInstallDialog, { onClose: closeInstall });
|
|
73
|
+
}
|
|
74
|
+
// Get selected plugin details
|
|
75
|
+
const selectedPlugin = installedPlugins[selectedIndex];
|
|
76
|
+
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: "Plugin Manager" }) }), isLoading && (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.colors.warning, children: "Loading..." }) })), error && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.colors.error, children: ["Error: ", error] }) })), lastAction && !error && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.colors.success, children: ["\u2713 ", lastAction] }) })), _jsx(PluginsList, { plugins: installedPlugins, selectedIndex: selectedIndex }), selectedPlugin && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.dim, paddingX: 1, marginTop: 1, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: selectedPlugin.manifest.name }), _jsx(Text, { color: theme.colors.dim, children: selectedPlugin.manifest.description }), _jsxs(Text, { color: theme.colors.dim, children: ["Author: ", selectedPlugin.manifest.author] }), _jsxs(Text, { color: theme.colors.dim, children: ["Permissions: ", selectedPlugin.manifest.permissions.join(', ')] })] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.colors.dim, children: [_jsx(Text, { color: theme.colors.text, children: "i" }), "=Install", ' ', _jsx(Text, { color: theme.colors.text, children: "e" }), "=Enable/Disable", ' ', _jsx(Text, { color: theme.colors.text, children: "r" }), "=Remove", ' ', _jsx(Text, { color: theme.colors.text, children: "u" }), "=Update", ' ', _jsx(Text, { color: theme.colors.text, children: "Esc" }), "=Back"] }) })] }));
|
|
77
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Search view layout
|
|
3
|
+
import { useNavigation } from "../../hooks/useNavigation.js";
|
|
4
|
+
import { useYouTubeMusic } from "../../hooks/useYouTubeMusic.js";
|
|
5
|
+
import SearchResults from "../search/SearchResults.js";
|
|
6
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { useTheme } from "../../hooks/useTheme.js";
|
|
9
|
+
import SearchBar from "../search/SearchBar.js";
|
|
10
|
+
import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
11
|
+
import { KEYBINDINGS } from "../../utils/constants.js";
|
|
12
|
+
import { Box, Text } from 'ink';
|
|
13
|
+
function SearchLayout() {
|
|
14
|
+
const { theme } = useTheme();
|
|
15
|
+
const { state: navState, dispatch } = useNavigation();
|
|
16
|
+
const { isLoading, error, search } = useYouTubeMusic();
|
|
17
|
+
const [results, setResults] = useState([]);
|
|
18
|
+
const [isTyping, setIsTyping] = useState(true);
|
|
19
|
+
const [isSearching, setIsSearching] = useState(false);
|
|
20
|
+
// Handle search action
|
|
21
|
+
const performSearch = useCallback(async (query) => {
|
|
22
|
+
if (!query || isSearching)
|
|
23
|
+
return;
|
|
24
|
+
setIsSearching(true);
|
|
25
|
+
const response = await search(query, {
|
|
26
|
+
type: navState.searchType,
|
|
27
|
+
limit: navState.searchLimit,
|
|
28
|
+
});
|
|
29
|
+
if (response) {
|
|
30
|
+
setResults(response.results);
|
|
31
|
+
dispatch({ category: 'SET_SELECTED_RESULT', index: 0 });
|
|
32
|
+
dispatch({ category: 'SET_HAS_SEARCHED', hasSearched: true });
|
|
33
|
+
// Defer focus switch to avoid consuming the same Enter key
|
|
34
|
+
// Use longer delay to ensure key event has been fully processed
|
|
35
|
+
setTimeout(() => setIsTyping(false), 100);
|
|
36
|
+
}
|
|
37
|
+
setIsSearching(false);
|
|
38
|
+
}, [search, navState.searchType, navState.searchLimit, dispatch, isSearching]);
|
|
39
|
+
// Adjust results limit
|
|
40
|
+
const increaseLimit = useCallback(() => {
|
|
41
|
+
dispatch({ category: 'SET_SEARCH_LIMIT', limit: navState.searchLimit + 5 });
|
|
42
|
+
}, [navState.searchLimit, dispatch]);
|
|
43
|
+
const decreaseLimit = useCallback(() => {
|
|
44
|
+
dispatch({ category: 'SET_SEARCH_LIMIT', limit: navState.searchLimit - 5 });
|
|
45
|
+
}, [navState.searchLimit, dispatch]);
|
|
46
|
+
useKeyBinding(KEYBINDINGS.INCREASE_RESULTS, increaseLimit);
|
|
47
|
+
useKeyBinding(KEYBINDINGS.DECREASE_RESULTS, decreaseLimit);
|
|
48
|
+
// Initial search if query is in state (usually from CLI flags)
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (navState.searchQuery && !navState.hasSearched) {
|
|
51
|
+
void performSearch(navState.searchQuery);
|
|
52
|
+
}
|
|
53
|
+
// We only want this to run once on mount or when searchQuery changes initially
|
|
54
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
55
|
+
}, []);
|
|
56
|
+
// Handle going back
|
|
57
|
+
const goBack = useCallback(() => {
|
|
58
|
+
if (!isTyping) {
|
|
59
|
+
setIsTyping(true); // Back to typing if in results
|
|
60
|
+
dispatch({ category: 'SET_HAS_SEARCHED', hasSearched: false });
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
dispatch({ category: 'GO_BACK' });
|
|
64
|
+
}
|
|
65
|
+
}, [isTyping, dispatch]);
|
|
66
|
+
useKeyBinding(KEYBINDINGS.BACK, goBack);
|
|
67
|
+
// Reset search state when leaving view
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
return () => {
|
|
70
|
+
setResults([]);
|
|
71
|
+
dispatch({ category: 'SET_HAS_SEARCHED', hasSearched: false });
|
|
72
|
+
dispatch({ category: 'SET_SEARCH_QUERY', query: '' });
|
|
73
|
+
};
|
|
74
|
+
}, [dispatch]);
|
|
75
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { borderStyle: "single", borderColor: theme.colors.secondary, paddingX: 1, children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: "Search" }), _jsxs(Text, { color: theme.colors.dim, children: [' ', "| Limit: ", navState.searchLimit, " (Use [ or ] to adjust)"] })] }), _jsx(SearchBar, { isActive: isTyping && !isSearching, onInput: input => {
|
|
76
|
+
void performSearch(input);
|
|
77
|
+
} }), (isLoading || isSearching) && (_jsx(Text, { color: theme.colors.accent, children: "Searching..." })), error && _jsx(Text, { color: theme.colors.error, children: error }), !isLoading && navState.hasSearched && (_jsx(SearchResults, { results: results, selectedIndex: navState.selectedResult, isActive: !isTyping })), !isLoading && navState.hasSearched && results.length === 0 && !error && (_jsx(Text, { color: theme.colors.dim, children: "No results found" })), _jsx(Text, { color: theme.colors.dim, children: isTyping
|
|
78
|
+
? 'Type to search, Enter to start'
|
|
79
|
+
: 'Arrows to navigate, Enter to play, Esc to type again' })] }));
|
|
80
|
+
}
|
|
81
|
+
export default React.memo(SearchLayout);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function NowPlaying(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
export default function NowPlaying() {
|
|
9
|
+
const { theme } = useTheme();
|
|
10
|
+
const { state: playerState } = usePlayer();
|
|
11
|
+
const { columns } = useTerminalSize();
|
|
12
|
+
if (!playerState.currentTrack) {
|
|
13
|
+
return (_jsx(Box, { borderStyle: "round", borderColor: theme.colors.dim, padding: 1, marginY: 1, children: _jsx(Text, { color: theme.colors.dim, children: "No track playing" }) }));
|
|
14
|
+
}
|
|
15
|
+
const track = playerState.currentTrack;
|
|
16
|
+
const artists = track.artists?.map(a => a.name).join(', ') || 'Unknown Artist';
|
|
17
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.colors.primary, padding: 1, marginY: 1, children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: track.title }), _jsx(Text, { color: theme.colors.secondary, children: artists }), track.album && _jsx(Text, { color: theme.colors.dim, children: track.album.name }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.text, children: formatTime(playerState.progress) }), _jsx(Text, { children: " " }), _jsxs(Text, { color: theme.colors.dim, children: ["[", Math.round((playerState.progress / (playerState.duration || 1)) * 100), "%]"] }), _jsx(Text, { children: " " }), _jsx(Text, { color: theme.colors.text, children: formatTime(playerState.duration) })] }), playerState.duration > 0 && (_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.primary, children: '■'.repeat(Math.floor((playerState.progress / playerState.duration) * (columns - 10))) }), _jsx(Text, { color: theme.colors.dim, children: '-'.repeat(Math.max(0, columns -
|
|
18
|
+
10 -
|
|
19
|
+
Math.floor((playerState.progress / playerState.duration) *
|
|
20
|
+
(columns - 10)))) })] })), playerState.isLoading && (_jsx(Text, { color: theme.colors.accent, children: "Loading..." })), playerState.error && (_jsx(Text, { color: theme.colors.error, children: playerState.error }))] }));
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function PlayerControls(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
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 } from 'react';
|
|
9
|
+
import { logger } from "../../services/logger/logger.service.js";
|
|
10
|
+
let mountCount = 0;
|
|
11
|
+
export default function PlayerControls() {
|
|
12
|
+
const instanceId = ++mountCount;
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
logger.debug('PlayerControls', 'Component mounted', { instanceId });
|
|
15
|
+
return () => {
|
|
16
|
+
logger.debug('PlayerControls', 'Component unmounted', { instanceId });
|
|
17
|
+
};
|
|
18
|
+
}, [instanceId]);
|
|
19
|
+
const { theme } = useTheme();
|
|
20
|
+
const { state: playerState, pause, resume, next, previous, volumeUp, volumeDown, } = usePlayer();
|
|
21
|
+
// DEBUG: Log when callbacks change (detect instability)
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
// Temporarily output to stderr to debug without triggering Ink re-render
|
|
24
|
+
process.stderr.write(`[PlayerControls] volumeUp callback: ${typeof volumeUp}\n`);
|
|
25
|
+
}, [volumeUp, instanceId]);
|
|
26
|
+
const handlePlayPause = () => {
|
|
27
|
+
if (playerState.isPlaying) {
|
|
28
|
+
pause();
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
resume();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
// Keyboard bindings
|
|
35
|
+
useKeyBinding(KEYBINDINGS.PLAY_PAUSE, handlePlayPause);
|
|
36
|
+
useKeyBinding(KEYBINDINGS.NEXT, next);
|
|
37
|
+
useKeyBinding(KEYBINDINGS.PREVIOUS, previous);
|
|
38
|
+
useKeyBinding(KEYBINDINGS.VOLUME_UP, volumeUp);
|
|
39
|
+
useKeyBinding(KEYBINDINGS.VOLUME_DOWN, volumeDown);
|
|
40
|
+
return (_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: "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: "n" }), "] Next"] }), _jsxs(Text, { color: theme.colors.text, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "+/-" }), "] Vol: ", playerState.volume, "%"] })] }));
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function ProgressBar(): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } 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
|
+
const progress = playerState.progress;
|
|
14
|
+
const duration = playerState.duration;
|
|
15
|
+
const percentage = duration > 0 ? Math.floor((progress / duration) * 100) : 0;
|
|
16
|
+
const barWidth = Math.max(0, Math.min(20, Math.floor(percentage / 5))); // 20 chars max, bounds checked
|
|
17
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.text, children: [formatTime(progress), " / ", formatTime(duration)] }), _jsxs(Text, { color: theme.colors.dim, children: [" ", percentage, "%"] })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.primary, children: '■'.repeat(barWidth) }), _jsx(Text, { color: theme.colors.dim, children: '-'.repeat(20 - barWidth) })] })] }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } 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 (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.colors.dim, children: "Queue is empty" }) }));
|
|
22
|
+
}
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { borderStyle: "double", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 1, children: _jsxs(Text, { bold: true, color: theme.colors.primary, children: ["Queue (", playerState.queue.length, " tracks)"] }) }), playerState.queue.map((track, index) => {
|
|
24
|
+
const isSelected = index === selectedIndex;
|
|
25
|
+
const artists = track.artists?.map(a => a.name).join(', ') || 'Unknown';
|
|
26
|
+
const title = truncate(track.title, getTruncateLength(50));
|
|
27
|
+
return (_jsxs(Box, { paddingX: 1, borderStyle: isSelected ? 'double' : undefined, borderColor: isSelected ? theme.colors.primary : undefined, children: [_jsxs(Text, { color: theme.colors.dim, children: [index + 1, "."] }), _jsx(Text, { color: isSelected ? theme.colors.primary : theme.colors.text, bold: isSelected, children: title }), _jsxs(Text, { color: theme.colors.dim, children: [' - ', artists] })] }, track.videoId));
|
|
28
|
+
})] }));
|
|
29
|
+
}
|
|
30
|
+
export default React.memo(QueueList);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function Suggestions(): import("react/jsx-runtime").JSX.Element;
|