@involvex/youtube-music-cli 0.0.47 → 0.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/cli.js.map +6 -6
- package/dist/youtube-music-cli +0 -0
- package/package.json +1 -1
- package/dist/eslint.config.js +0 -55
- package/dist/package.json +0 -120
- package/dist/scripts/build-cli.js +0 -46
- package/dist/source/app.js +0 -17
- package/dist/source/cli.js +0 -504
- package/dist/source/components/common/ErrorBoundary.js +0 -22
- package/dist/source/components/common/Help.js +0 -18
- package/dist/source/components/common/ShortcutsBar.js +0 -89
- package/dist/source/components/config/ConfigLayout.js +0 -84
- package/dist/source/components/config/KeybindingsLayout.js +0 -107
- package/dist/source/components/export/ExportLayout.js +0 -111
- package/dist/source/components/import/ImportLayout.js +0 -119
- package/dist/source/components/import/ImportProgress.js +0 -73
- package/dist/source/components/layouts/ExploreLayout.js +0 -72
- package/dist/source/components/layouts/HistoryLayout.js +0 -37
- package/dist/source/components/layouts/LyricsLayout.js +0 -89
- package/dist/source/components/layouts/MainLayout.js +0 -190
- package/dist/source/components/layouts/MiniPlayerLayout.js +0 -20
- package/dist/source/components/layouts/PlayerLayout.js +0 -9
- package/dist/source/components/layouts/PluginsLayout.js +0 -77
- package/dist/source/components/layouts/SearchLayout.js +0 -193
- package/dist/source/components/layouts/TrendingLayout.js +0 -59
- package/dist/source/components/player/NowPlaying.js +0 -45
- package/dist/source/components/player/PlayerControls.js +0 -83
- package/dist/source/components/player/ProgressBar.js +0 -19
- package/dist/source/components/player/QueueList.js +0 -36
- package/dist/source/components/player/Suggestions.js +0 -50
- package/dist/source/components/playlist/PlaylistList.js +0 -138
- package/dist/source/components/plugins/PluginInstallDialog.js +0 -41
- package/dist/source/components/plugins/PluginsAvailable.js +0 -55
- package/dist/source/components/plugins/PluginsList.js +0 -18
- package/dist/source/components/search/SearchBar.js +0 -55
- package/dist/source/components/search/SearchHistory.js +0 -35
- package/dist/source/components/search/SearchResults.js +0 -280
- package/dist/source/components/settings/Settings.js +0 -211
- package/dist/source/components/theme/ThemeSwitcher.js +0 -11
- package/dist/source/config/themes.config.js +0 -123
- package/dist/source/contexts/theme.context.js +0 -29
- package/dist/source/hooks/useKeyboard.js +0 -188
- package/dist/source/hooks/useKeyboardBlocker.js +0 -45
- package/dist/source/hooks/useNavigation.js +0 -5
- package/dist/source/hooks/usePlayer.js +0 -43
- package/dist/source/hooks/usePlaylist.js +0 -65
- package/dist/source/hooks/useSearch.js +0 -76
- package/dist/source/hooks/useSleepTimer.js +0 -48
- package/dist/source/hooks/useTerminalSize.js +0 -24
- package/dist/source/hooks/useTheme.js +0 -5
- package/dist/source/hooks/useYouTubeMusic.js +0 -112
- package/dist/source/main.js +0 -127
- package/dist/source/services/cache/cache.service.js +0 -67
- package/dist/source/services/completions/completions.service.js +0 -313
- package/dist/source/services/config/config.service.js +0 -191
- package/dist/source/services/discord/discord-rpc.service.js +0 -95
- package/dist/source/services/download/download.service.js +0 -350
- package/dist/source/services/export/export.service.js +0 -131
- package/dist/source/services/history/history.service.js +0 -83
- package/dist/source/services/import/import.service.js +0 -272
- package/dist/source/services/import/spotify.service.js +0 -171
- package/dist/source/services/import/track-matcher.service.js +0 -271
- package/dist/source/services/import/youtube-import.service.js +0 -84
- package/dist/source/services/logger/logger.service.js +0 -52
- package/dist/source/services/lyrics/lyrics.service.js +0 -93
- package/dist/source/services/mpris/mpris.service.js +0 -78
- package/dist/source/services/notification/notification.service.js +0 -57
- package/dist/source/services/player/dependency-check.service.js +0 -140
- package/dist/source/services/player/player.service.js +0 -478
- package/dist/source/services/player-state/player-state.service.js +0 -123
- package/dist/source/services/plugin/plugin-audio-api.js +0 -36
- package/dist/source/services/plugin/plugin-context.js +0 -256
- package/dist/source/services/plugin/plugin-hooks.service.js +0 -135
- package/dist/source/services/plugin/plugin-installer.service.js +0 -248
- package/dist/source/services/plugin/plugin-loader.service.js +0 -161
- package/dist/source/services/plugin/plugin-permissions.service.js +0 -194
- package/dist/source/services/plugin/plugin-registry.service.js +0 -215
- package/dist/source/services/plugin/plugin-ui-api.js +0 -46
- package/dist/source/services/plugin/plugin-updater.service.js +0 -206
- package/dist/source/services/scrobbling/scrobbling.service.js +0 -115
- package/dist/source/services/sleep-timer/sleep-timer.service.js +0 -45
- package/dist/source/services/version-check/version-check.service.js +0 -121
- package/dist/source/services/web/static-file.service.js +0 -185
- package/dist/source/services/web/web-server-manager.js +0 -507
- package/dist/source/services/web/web-streaming.service.js +0 -292
- package/dist/source/services/web/websocket.server.js +0 -267
- package/dist/source/services/youtube-music/api.js +0 -649
- package/dist/source/services/youtube-music/search.service.js +0 -38
- package/dist/source/stores/history.store.js +0 -64
- package/dist/source/stores/navigation.store.js +0 -90
- package/dist/source/stores/player.store.js +0 -789
- package/dist/source/stores/plugins.store.js +0 -177
- package/dist/source/types/actions.js +0 -1
- package/dist/source/types/cli.types.js +0 -1
- package/dist/source/types/config.types.js +0 -1
- package/dist/source/types/history.types.js +0 -1
- package/dist/source/types/import.types.js +0 -2
- package/dist/source/types/keyboard.types.js +0 -1
- package/dist/source/types/navigation.types.js +0 -1
- package/dist/source/types/player.types.js +0 -1
- package/dist/source/types/playlist.types.js +0 -1
- package/dist/source/types/plugin.types.js +0 -1
- package/dist/source/types/theme.types.js +0 -1
- package/dist/source/types/web.types.js +0 -2
- package/dist/source/types/youtube-music.types.js +0 -1
- package/dist/source/types/youtubei.types.js +0 -3
- package/dist/source/utils/constants.js +0 -135
- package/dist/source/utils/format.js +0 -24
- package/dist/source/utils/icons.js +0 -28
- package/dist/source/utils/search-filters.js +0 -100
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// Lyrics view layout - displays synced or plain lyrics
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
4
|
-
import { Box, Text } from 'ink';
|
|
5
|
-
import { useTheme } from "../../hooks/useTheme.js";
|
|
6
|
-
import { usePlayer } from "../../hooks/usePlayer.js";
|
|
7
|
-
import { getLyricsService, } from "../../services/lyrics/lyrics.service.js";
|
|
8
|
-
import { useTerminalSize } from "../../hooks/useTerminalSize.js";
|
|
9
|
-
const CONTEXT_LINES = 3; // Lines shown before/after current line
|
|
10
|
-
export default function LyricsLayout() {
|
|
11
|
-
const { theme } = useTheme();
|
|
12
|
-
const { state } = usePlayer();
|
|
13
|
-
const { rows } = useTerminalSize();
|
|
14
|
-
const [lyrics, setLyrics] = useState(null);
|
|
15
|
-
const [loading, setLoading] = useState(false);
|
|
16
|
-
const [error, setError] = useState(null);
|
|
17
|
-
const lyricsService = getLyricsService();
|
|
18
|
-
// Fetch lyrics when track changes
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
const track = state.currentTrack;
|
|
21
|
-
let cancelled = false;
|
|
22
|
-
if (!track) {
|
|
23
|
-
queueMicrotask(() => {
|
|
24
|
-
if (!cancelled) {
|
|
25
|
-
setLyrics(null);
|
|
26
|
-
setLoading(false);
|
|
27
|
-
setError(null);
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
const artist = track.artists?.[0]?.name ?? '';
|
|
33
|
-
queueMicrotask(() => {
|
|
34
|
-
if (!cancelled) {
|
|
35
|
-
setLoading(true);
|
|
36
|
-
setError(null);
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
void lyricsService
|
|
40
|
-
.getLyrics(track.title, artist, state.duration || undefined)
|
|
41
|
-
.then(result => {
|
|
42
|
-
if (cancelled) {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
setLyrics(result);
|
|
46
|
-
setLoading(false);
|
|
47
|
-
if (!result) {
|
|
48
|
-
setError('No lyrics found');
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
.catch(() => {
|
|
52
|
-
if (cancelled) {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
setLoading(false);
|
|
56
|
-
setError('Failed to load lyrics');
|
|
57
|
-
});
|
|
58
|
-
return () => {
|
|
59
|
-
cancelled = true;
|
|
60
|
-
};
|
|
61
|
-
}, [lyricsService, state.currentTrack, state.duration]);
|
|
62
|
-
const track = state.currentTrack;
|
|
63
|
-
const title = track?.title ?? 'No track playing';
|
|
64
|
-
const artist = track?.artists?.map(a => a.name).join(', ') ?? '';
|
|
65
|
-
// Determine current line
|
|
66
|
-
const currentLineIndex = lyrics?.synced
|
|
67
|
-
? lyricsService.getCurrentLineIndex(lyrics.synced, state.progress)
|
|
68
|
-
: -1;
|
|
69
|
-
// Calculate visible lines window
|
|
70
|
-
const visibleLines = (() => {
|
|
71
|
-
if (!lyrics?.synced)
|
|
72
|
-
return null;
|
|
73
|
-
const start = Math.max(0, currentLineIndex - CONTEXT_LINES);
|
|
74
|
-
const maxLines = Math.max(5, rows - 8);
|
|
75
|
-
const end = Math.min(lyrics.synced.length, start + maxLines);
|
|
76
|
-
return lyrics.synced.slice(start, end).map((line, i) => ({
|
|
77
|
-
line,
|
|
78
|
-
globalIndex: start + i,
|
|
79
|
-
}));
|
|
80
|
-
})();
|
|
81
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { borderStyle: "double", borderColor: theme.colors.secondary, paddingX: 1, children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: title }), artist && _jsxs(Text, { color: theme.colors.secondary, children: [" \u2014 ", artist] })] }), loading && _jsx(Text, { color: theme.colors.accent, children: "Loading lyrics..." }), error && !loading && _jsx(Text, { color: theme.colors.dim, children: error }), !loading && visibleLines && (_jsx(Box, { flexDirection: "column", paddingX: 1, children: visibleLines.map(({ line, globalIndex }) => (_jsxs(Text, { bold: globalIndex === currentLineIndex, color: globalIndex === currentLineIndex
|
|
82
|
-
? theme.colors.primary
|
|
83
|
-
: globalIndex < currentLineIndex
|
|
84
|
-
? theme.colors.dim
|
|
85
|
-
: theme.colors.text, children: [globalIndex === currentLineIndex ? '▶ ' : ' ', line.text || '♪'] }, globalIndex))) })), !loading && !lyrics?.synced && lyrics?.plain && (_jsx(Box, { flexDirection: "column", paddingX: 1, children: lyrics.plain
|
|
86
|
-
.split('\n')
|
|
87
|
-
.slice(0, Math.max(5, rows - 8))
|
|
88
|
-
.map((line, i) => (_jsx(Text, { color: theme.colors.text, children: line || ' ' }, i))) })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.colors.dim, children: ["Press ", _jsx(Text, { color: theme.colors.text, children: "l" }), " or", ' ', _jsx(Text, { color: theme.colors.text, children: "Esc" }), " to go back"] }) })] }));
|
|
89
|
-
}
|
|
@@ -1,190 +0,0 @@
|
|
|
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 MiniPlayerLayout from "./MiniPlayerLayout.js";
|
|
13
|
-
import PluginsLayout from "./PluginsLayout.js";
|
|
14
|
-
import Suggestions from "../player/Suggestions.js";
|
|
15
|
-
import Settings from "../settings/Settings.js";
|
|
16
|
-
import ConfigLayout from "../config/ConfigLayout.js";
|
|
17
|
-
import ShortcutsBar from "../common/ShortcutsBar.js";
|
|
18
|
-
import LyricsLayout from "./LyricsLayout.js";
|
|
19
|
-
import SearchHistory from "../search/SearchHistory.js";
|
|
20
|
-
import KeybindingsLayout from "../config/KeybindingsLayout.js";
|
|
21
|
-
import TrendingLayout from "./TrendingLayout.js";
|
|
22
|
-
import ExploreLayout from "./ExploreLayout.js";
|
|
23
|
-
import HistoryLayout from "./HistoryLayout.js";
|
|
24
|
-
import ImportLayout from "../import/ImportLayout.js";
|
|
25
|
-
import ExportLayout from "../export/ExportLayout.js";
|
|
26
|
-
import { KEYBINDINGS, VIEW } from "../../utils/constants.js";
|
|
27
|
-
import { Box } from 'ink';
|
|
28
|
-
import { useTerminalSize } from "../../hooks/useTerminalSize.js";
|
|
29
|
-
import { getPlayerService } from "../../services/player/player.service.js";
|
|
30
|
-
import { getConfigService } from "../../services/config/config.service.js";
|
|
31
|
-
import { usePlayer } from "../../hooks/usePlayer.js";
|
|
32
|
-
function MainLayout() {
|
|
33
|
-
const { theme } = useTheme();
|
|
34
|
-
const { state: navState, dispatch } = useNavigation();
|
|
35
|
-
const { resume } = usePlayer();
|
|
36
|
-
const { columns } = useTerminalSize();
|
|
37
|
-
// Responsive padding based on terminal size
|
|
38
|
-
const getPadding = () => (columns < 100 ? 0 : 1);
|
|
39
|
-
// Navigate to different views
|
|
40
|
-
const goToSearch = useCallback(() => {
|
|
41
|
-
dispatch({ category: 'NAVIGATE', view: VIEW.SEARCH });
|
|
42
|
-
}, [dispatch]);
|
|
43
|
-
const goToPlaylists = useCallback(() => {
|
|
44
|
-
dispatch({ category: 'NAVIGATE', view: VIEW.PLAYLISTS });
|
|
45
|
-
}, [dispatch]);
|
|
46
|
-
const goToSuggestions = useCallback(() => {
|
|
47
|
-
dispatch({ category: 'NAVIGATE', view: VIEW.SUGGESTIONS });
|
|
48
|
-
}, [dispatch]);
|
|
49
|
-
const goToPlugins = useCallback(() => {
|
|
50
|
-
dispatch({ category: 'NAVIGATE', view: VIEW.PLUGINS });
|
|
51
|
-
}, [dispatch]);
|
|
52
|
-
const goToSettings = useCallback(() => {
|
|
53
|
-
dispatch({ category: 'NAVIGATE', view: VIEW.SETTINGS });
|
|
54
|
-
}, [dispatch]);
|
|
55
|
-
const goToHistory = useCallback(() => {
|
|
56
|
-
dispatch({ category: 'NAVIGATE', view: VIEW.HISTORY });
|
|
57
|
-
}, [dispatch]);
|
|
58
|
-
const goToHelp = useCallback(() => {
|
|
59
|
-
if (navState.currentView === VIEW.HELP) {
|
|
60
|
-
dispatch({ category: 'GO_BACK' });
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
dispatch({ category: 'NAVIGATE', view: VIEW.HELP });
|
|
64
|
-
}, [dispatch, navState.currentView]);
|
|
65
|
-
const handleQuit = useCallback(() => {
|
|
66
|
-
// From player view, quit the app
|
|
67
|
-
if (navState.currentView === VIEW.PLAYER) {
|
|
68
|
-
process.exit(0);
|
|
69
|
-
}
|
|
70
|
-
// From other views, go back
|
|
71
|
-
dispatch({ category: 'GO_BACK' });
|
|
72
|
-
}, [navState.currentView, dispatch]);
|
|
73
|
-
const goToLyrics = useCallback(() => {
|
|
74
|
-
dispatch({ category: 'NAVIGATE', view: VIEW.LYRICS });
|
|
75
|
-
}, [dispatch]);
|
|
76
|
-
const goToTrending = useCallback(() => {
|
|
77
|
-
dispatch({ category: 'NAVIGATE', view: VIEW.TRENDING });
|
|
78
|
-
}, [dispatch]);
|
|
79
|
-
const goToExplore = useCallback(() => {
|
|
80
|
-
// Don't navigate to explore if we're in plugins view (e key is used for enable/disable there)
|
|
81
|
-
if (navState.currentView !== VIEW.PLUGINS) {
|
|
82
|
-
dispatch({ category: 'NAVIGATE', view: VIEW.EXPLORE });
|
|
83
|
-
}
|
|
84
|
-
}, [dispatch, navState.currentView]);
|
|
85
|
-
const goToImport = useCallback(() => {
|
|
86
|
-
// Don't navigate to import if we're in plugins view (i key is used for plugin install there)
|
|
87
|
-
// Don't navigate to import if we're in settings view (user navigates settings items with Enter)
|
|
88
|
-
if (navState.currentView !== VIEW.PLUGINS &&
|
|
89
|
-
navState.currentView !== VIEW.SETTINGS) {
|
|
90
|
-
dispatch({ category: 'NAVIGATE', view: VIEW.IMPORT });
|
|
91
|
-
}
|
|
92
|
-
}, [dispatch, navState.currentView]);
|
|
93
|
-
const handleDetach = useCallback(() => {
|
|
94
|
-
// Detach mode: Exit CLI while keeping music playing
|
|
95
|
-
const player = getPlayerService();
|
|
96
|
-
const config = getConfigService();
|
|
97
|
-
// Get the IPC path and current URL before detaching
|
|
98
|
-
const { ipcPath, currentUrl } = player.detach();
|
|
99
|
-
// Save the background playback state if we have an active session
|
|
100
|
-
if (ipcPath && currentUrl) {
|
|
101
|
-
config.setBackgroundPlaybackState({ ipcPath, currentUrl });
|
|
102
|
-
}
|
|
103
|
-
// Exit the app
|
|
104
|
-
process.exit(0);
|
|
105
|
-
}, []);
|
|
106
|
-
const handleResumeBackground = useCallback(() => {
|
|
107
|
-
const player = getPlayerService();
|
|
108
|
-
const config = getConfigService();
|
|
109
|
-
const backgroundState = config.getBackgroundPlaybackState();
|
|
110
|
-
if (!backgroundState.enabled || !backgroundState.ipcPath) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
void player
|
|
114
|
-
.reattach(backgroundState.ipcPath)
|
|
115
|
-
.then(() => {
|
|
116
|
-
resume();
|
|
117
|
-
config.clearBackgroundPlaybackState();
|
|
118
|
-
})
|
|
119
|
-
.catch(() => {
|
|
120
|
-
config.clearBackgroundPlaybackState();
|
|
121
|
-
});
|
|
122
|
-
}, [resume]);
|
|
123
|
-
const togglePlayerMode = useCallback(() => {
|
|
124
|
-
dispatch({ category: 'TOGGLE_PLAYER_MODE' });
|
|
125
|
-
}, [dispatch]);
|
|
126
|
-
// Global keyboard bindings
|
|
127
|
-
useKeyBinding(KEYBINDINGS.QUIT, handleQuit);
|
|
128
|
-
useKeyBinding(KEYBINDINGS.SEARCH, goToSearch);
|
|
129
|
-
useKeyBinding(KEYBINDINGS.PLAYLISTS, goToPlaylists);
|
|
130
|
-
useKeyBinding(KEYBINDINGS.PLUGINS, goToPlugins);
|
|
131
|
-
useKeyBinding(KEYBINDINGS.SUGGESTIONS, goToSuggestions);
|
|
132
|
-
useKeyBinding(KEYBINDINGS.HISTORY, goToHistory);
|
|
133
|
-
useKeyBinding(KEYBINDINGS.SETTINGS, goToSettings);
|
|
134
|
-
useKeyBinding(KEYBINDINGS.HELP, goToHelp);
|
|
135
|
-
useKeyBinding(['M'], togglePlayerMode);
|
|
136
|
-
useKeyBinding(['l'], goToLyrics);
|
|
137
|
-
useKeyBinding(['T'], goToTrending);
|
|
138
|
-
useKeyBinding(['e'], goToExplore);
|
|
139
|
-
useKeyBinding(['i'], goToImport);
|
|
140
|
-
useKeyBinding(KEYBINDINGS.DETACH, handleDetach);
|
|
141
|
-
useKeyBinding(KEYBINDINGS.RESUME_BACKGROUND, handleResumeBackground);
|
|
142
|
-
// Memoize the view component to prevent unnecessary remounts
|
|
143
|
-
// Only recreate when currentView actually changes
|
|
144
|
-
const currentView = useMemo(() => {
|
|
145
|
-
// In mini mode, only show the mini player bar
|
|
146
|
-
if (navState.playerMode === 'mini') {
|
|
147
|
-
return _jsx(MiniPlayerLayout, {}, "mini-player");
|
|
148
|
-
}
|
|
149
|
-
switch (navState.currentView) {
|
|
150
|
-
case 'player':
|
|
151
|
-
return _jsx(PlayerLayout, {}, "player");
|
|
152
|
-
case 'search':
|
|
153
|
-
return _jsx(SearchLayout, {}, "search");
|
|
154
|
-
case 'search_history':
|
|
155
|
-
return (_jsx(SearchHistory, { onSelect: query => {
|
|
156
|
-
dispatch({ category: 'SET_SEARCH_QUERY', query });
|
|
157
|
-
} }, "search_history"));
|
|
158
|
-
case 'playlists':
|
|
159
|
-
return _jsx(PlaylistList, {}, "playlists");
|
|
160
|
-
case 'suggestions':
|
|
161
|
-
return _jsx(Suggestions, {}, "suggestions");
|
|
162
|
-
case 'history':
|
|
163
|
-
return _jsx(HistoryLayout, {}, "history");
|
|
164
|
-
case 'settings':
|
|
165
|
-
return _jsx(Settings, {}, "settings");
|
|
166
|
-
case 'plugins':
|
|
167
|
-
return _jsx(PluginsLayout, {}, "plugins");
|
|
168
|
-
case 'config':
|
|
169
|
-
return _jsx(ConfigLayout, {}, "config");
|
|
170
|
-
case 'lyrics':
|
|
171
|
-
return _jsx(LyricsLayout, {}, "lyrics");
|
|
172
|
-
case 'keybindings':
|
|
173
|
-
return _jsx(KeybindingsLayout, {}, "keybindings");
|
|
174
|
-
case 'trending':
|
|
175
|
-
return _jsx(TrendingLayout, {}, "trending");
|
|
176
|
-
case 'explore':
|
|
177
|
-
return _jsx(ExploreLayout, {}, "explore");
|
|
178
|
-
case 'import':
|
|
179
|
-
return _jsx(ImportLayout, {}, "import");
|
|
180
|
-
case 'export_playlists':
|
|
181
|
-
return _jsx(ExportLayout, {}, "export_playlists");
|
|
182
|
-
case 'help':
|
|
183
|
-
return _jsx(Help, {}, "help");
|
|
184
|
-
default:
|
|
185
|
-
return _jsx(PlayerLayout, {}, "player-default");
|
|
186
|
-
}
|
|
187
|
-
}, [navState.currentView, navState.playerMode, dispatch]);
|
|
188
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: getPadding(), borderStyle: "single", borderColor: theme.colors.primary, children: [currentView, _jsx(ShortcutsBar, {})] }));
|
|
189
|
-
}
|
|
190
|
-
export default React.memo(MainLayout);
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// Mini player layout - compact single-line player
|
|
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 { ICONS } from "../../utils/icons.js";
|
|
8
|
-
export default function MiniPlayerLayout() {
|
|
9
|
-
const { theme } = useTheme();
|
|
10
|
-
const { state } = usePlayer();
|
|
11
|
-
const track = state.currentTrack;
|
|
12
|
-
const artist = track?.artists?.map(a => a.name).join(', ') ?? 'Unknown';
|
|
13
|
-
const title = track?.title ?? 'No track playing';
|
|
14
|
-
const progress = formatTime(state.progress);
|
|
15
|
-
const duration = formatTime(state.duration);
|
|
16
|
-
const playIcon = state.isPlaying ? ICONS.PLAY : ICONS.PAUSE;
|
|
17
|
-
const vol = `${state.volume}%`;
|
|
18
|
-
const speed = (state.speed ?? 1.0) !== 1.0 ? ` ${(state.speed ?? 1.0).toFixed(2)}x` : '';
|
|
19
|
-
return (_jsxs(Box, { flexDirection: "row", paddingX: 1, gap: 1, children: [_jsx(Text, { color: state.isPlaying ? theme.colors.success : theme.colors.dim, children: playIcon }), _jsx(Text, { bold: true, color: theme.colors.primary, children: title }), _jsx(Text, { color: theme.colors.dim, children: "\u2014" }), _jsx(Text, { color: theme.colors.secondary, children: artist }), _jsx(Text, { color: theme.colors.dim, children: "|" }), _jsxs(Text, { color: theme.colors.text, children: [progress, "/", duration] }), _jsx(Text, { color: theme.colors.dim, children: "|" }), _jsxs(Text, { color: theme.colors.text, children: ["vol:", vol] }), speed && _jsx(Text, { color: theme.colors.accent, children: speed }), state.isLoading && _jsx(Text, { color: theme.colors.accent, children: "Loading..." })] }));
|
|
20
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
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 QueueList from "../player/QueueList.js";
|
|
5
|
-
import { Box } from 'ink';
|
|
6
|
-
export default function PlayerLayout() {
|
|
7
|
-
const { state: playerState } = usePlayer();
|
|
8
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(NowPlaying, {}), playerState.queue.length > 0 && _jsx(QueueList, {})] }));
|
|
9
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,193 +0,0 @@
|
|
|
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, useRef, useMemo } 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, VIEW } from "../../utils/constants.js";
|
|
12
|
-
import { Box, Text } from 'ink';
|
|
13
|
-
import { usePlayer } from "../../hooks/usePlayer.js";
|
|
14
|
-
import { ICONS } from "../../utils/icons.js";
|
|
15
|
-
import TextInput from 'ink-text-input';
|
|
16
|
-
import { applySearchFilters } from "../../utils/search-filters.js";
|
|
17
|
-
const FILTER_LABELS = {
|
|
18
|
-
artist: 'Artist',
|
|
19
|
-
album: 'Album',
|
|
20
|
-
year: 'Year',
|
|
21
|
-
};
|
|
22
|
-
const DURATION_ORDER = [
|
|
23
|
-
'all',
|
|
24
|
-
'short',
|
|
25
|
-
'medium',
|
|
26
|
-
'long',
|
|
27
|
-
];
|
|
28
|
-
function SearchLayout() {
|
|
29
|
-
const { theme } = useTheme();
|
|
30
|
-
const { state: navState, dispatch } = useNavigation();
|
|
31
|
-
const { state: playerState } = usePlayer();
|
|
32
|
-
const { isLoading, error, search } = useYouTubeMusic();
|
|
33
|
-
const [rawResults, setRawResults] = useState([]);
|
|
34
|
-
const filteredResults = useMemo(() => applySearchFilters(rawResults, navState.searchFilters), [rawResults, navState.searchFilters]);
|
|
35
|
-
const [isTyping, setIsTyping] = useState(true);
|
|
36
|
-
const [isSearching, setIsSearching] = useState(false);
|
|
37
|
-
const [actionMessage, setActionMessage] = useState(null);
|
|
38
|
-
const actionTimeoutRef = useRef(null);
|
|
39
|
-
const lastAutoSearchedQueryRef = useRef(null);
|
|
40
|
-
const [editingFilter, setEditingFilter] = useState(null);
|
|
41
|
-
const [filterDraft, setFilterDraft] = useState('');
|
|
42
|
-
const describeFilterValue = (value) => value?.trim() ? value.trim() : 'Any';
|
|
43
|
-
const handleFilterSubmit = useCallback((value) => {
|
|
44
|
-
if (!editingFilter)
|
|
45
|
-
return;
|
|
46
|
-
dispatch({
|
|
47
|
-
category: 'SET_SEARCH_FILTERS',
|
|
48
|
-
filters: { [editingFilter]: value.trim() },
|
|
49
|
-
});
|
|
50
|
-
setEditingFilter(null);
|
|
51
|
-
setFilterDraft('');
|
|
52
|
-
}, [dispatch, editingFilter]);
|
|
53
|
-
const beginFilterEdit = useCallback((field) => {
|
|
54
|
-
setEditingFilter(field);
|
|
55
|
-
setFilterDraft(navState.searchFilters[field] ?? '');
|
|
56
|
-
}, [navState.searchFilters]);
|
|
57
|
-
const cycleDurationFilter = useCallback(() => {
|
|
58
|
-
const currentIndex = DURATION_ORDER.indexOf(navState.searchFilters.duration ?? 'all');
|
|
59
|
-
const nextIndex = (currentIndex + 1) % DURATION_ORDER.length;
|
|
60
|
-
const nextDuration = DURATION_ORDER[nextIndex];
|
|
61
|
-
dispatch({
|
|
62
|
-
category: 'SET_SEARCH_FILTERS',
|
|
63
|
-
filters: { duration: nextDuration },
|
|
64
|
-
});
|
|
65
|
-
}, [dispatch, navState.searchFilters.duration]);
|
|
66
|
-
// Handle search action
|
|
67
|
-
const performSearch = useCallback(async (query) => {
|
|
68
|
-
if (!query || isSearching)
|
|
69
|
-
return;
|
|
70
|
-
setIsSearching(true);
|
|
71
|
-
const response = await search(query, {
|
|
72
|
-
type: navState.searchType,
|
|
73
|
-
limit: navState.searchLimit,
|
|
74
|
-
});
|
|
75
|
-
if (response) {
|
|
76
|
-
setRawResults(response.results);
|
|
77
|
-
dispatch({ category: 'SET_SELECTED_RESULT', index: 0 });
|
|
78
|
-
dispatch({ category: 'SET_HAS_SEARCHED', hasSearched: true });
|
|
79
|
-
// Defer focus switch to avoid consuming the same Enter key
|
|
80
|
-
// Use longer delay to ensure key event has been fully processed
|
|
81
|
-
setTimeout(() => setIsTyping(false), 100);
|
|
82
|
-
}
|
|
83
|
-
setIsSearching(false);
|
|
84
|
-
}, [search, navState.searchType, navState.searchLimit, dispatch, isSearching]);
|
|
85
|
-
// Adjust results limit
|
|
86
|
-
const increaseLimit = useCallback(() => {
|
|
87
|
-
dispatch({ category: 'SET_SEARCH_LIMIT', limit: navState.searchLimit + 5 });
|
|
88
|
-
}, [navState.searchLimit, dispatch]);
|
|
89
|
-
const decreaseLimit = useCallback(() => {
|
|
90
|
-
dispatch({ category: 'SET_SEARCH_LIMIT', limit: navState.searchLimit - 5 });
|
|
91
|
-
}, [navState.searchLimit, dispatch]);
|
|
92
|
-
useKeyBinding(KEYBINDINGS.INCREASE_RESULTS, increaseLimit);
|
|
93
|
-
useKeyBinding(KEYBINDINGS.DECREASE_RESULTS, decreaseLimit);
|
|
94
|
-
// Open search history
|
|
95
|
-
const goToHistory = useCallback(() => {
|
|
96
|
-
if (!isTyping) {
|
|
97
|
-
dispatch({ category: 'NAVIGATE', view: VIEW.SEARCH_HISTORY });
|
|
98
|
-
}
|
|
99
|
-
}, [isTyping, dispatch]);
|
|
100
|
-
useKeyBinding(['h'], goToHistory);
|
|
101
|
-
useKeyBinding(KEYBINDINGS.SEARCH_FILTER_ARTIST, () => beginFilterEdit('artist'));
|
|
102
|
-
useKeyBinding(KEYBINDINGS.SEARCH_FILTER_ALBUM, () => beginFilterEdit('album'));
|
|
103
|
-
useKeyBinding(KEYBINDINGS.SEARCH_FILTER_YEAR, () => beginFilterEdit('year'));
|
|
104
|
-
useKeyBinding(KEYBINDINGS.SEARCH_FILTER_DURATION, cycleDurationFilter);
|
|
105
|
-
// Initial search if query is in state (usually from CLI flags)
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
const query = navState.searchQuery.trim();
|
|
108
|
-
if (!query || navState.hasSearched) {
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
if (lastAutoSearchedQueryRef.current === query) {
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
lastAutoSearchedQueryRef.current = query;
|
|
115
|
-
queueMicrotask(() => {
|
|
116
|
-
void performSearch(query);
|
|
117
|
-
});
|
|
118
|
-
}, [navState.searchQuery, navState.hasSearched, performSearch]);
|
|
119
|
-
// Handle going back
|
|
120
|
-
const goBack = useCallback(() => {
|
|
121
|
-
if (editingFilter) {
|
|
122
|
-
setEditingFilter(null);
|
|
123
|
-
setFilterDraft('');
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
if (!isTyping) {
|
|
127
|
-
setIsTyping(true); // Back to typing if in results
|
|
128
|
-
dispatch({ category: 'SET_HAS_SEARCHED', hasSearched: false });
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
dispatch({ category: 'GO_BACK' });
|
|
132
|
-
}
|
|
133
|
-
}, [editingFilter, isTyping, dispatch]);
|
|
134
|
-
useKeyBinding(KEYBINDINGS.BACK, goBack);
|
|
135
|
-
const handleMixCreated = useCallback((message) => {
|
|
136
|
-
setActionMessage(message);
|
|
137
|
-
if (actionTimeoutRef.current) {
|
|
138
|
-
clearTimeout(actionTimeoutRef.current);
|
|
139
|
-
}
|
|
140
|
-
actionTimeoutRef.current = setTimeout(() => {
|
|
141
|
-
setActionMessage(null);
|
|
142
|
-
actionTimeoutRef.current = null;
|
|
143
|
-
}, 4000);
|
|
144
|
-
}, []);
|
|
145
|
-
const handleDownloadStatus = useCallback((message) => {
|
|
146
|
-
setActionMessage(message);
|
|
147
|
-
if (actionTimeoutRef.current) {
|
|
148
|
-
clearTimeout(actionTimeoutRef.current);
|
|
149
|
-
}
|
|
150
|
-
actionTimeoutRef.current = setTimeout(() => {
|
|
151
|
-
setActionMessage(null);
|
|
152
|
-
actionTimeoutRef.current = null;
|
|
153
|
-
}, 4000);
|
|
154
|
-
}, []);
|
|
155
|
-
useEffect(() => {
|
|
156
|
-
return () => {
|
|
157
|
-
if (actionTimeoutRef.current) {
|
|
158
|
-
clearTimeout(actionTimeoutRef.current);
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
}, []);
|
|
162
|
-
// Reset search state when leaving view
|
|
163
|
-
useEffect(() => {
|
|
164
|
-
return () => {
|
|
165
|
-
setRawResults([]);
|
|
166
|
-
dispatch({ category: 'SET_HAS_SEARCHED', hasSearched: false });
|
|
167
|
-
dispatch({ category: 'SET_SEARCH_QUERY', query: '' });
|
|
168
|
-
lastAutoSearchedQueryRef.current = null;
|
|
169
|
-
};
|
|
170
|
-
}, [dispatch]);
|
|
171
|
-
useEffect(() => {
|
|
172
|
-
if (filteredResults.length > 0 &&
|
|
173
|
-
navState.selectedResult >= filteredResults.length) {
|
|
174
|
-
dispatch({ category: 'SET_SELECTED_RESULT', index: 0 });
|
|
175
|
-
}
|
|
176
|
-
}, [dispatch, filteredResults.length, navState.selectedResult]);
|
|
177
|
-
const artistFilterLabel = describeFilterValue(navState.searchFilters.artist);
|
|
178
|
-
const albumFilterLabel = describeFilterValue(navState.searchFilters.album);
|
|
179
|
-
const yearFilterLabel = describeFilterValue(navState.searchFilters.year);
|
|
180
|
-
const durationFilterLabel = navState.searchFilters.duration && navState.searchFilters.duration !== 'all'
|
|
181
|
-
? navState.searchFilters.duration
|
|
182
|
-
: 'Any';
|
|
183
|
-
return (_jsxs(Box, { flexDirection: "column", children: [playerState.currentTrack && (_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.dim, children: playerState.isPlaying ? `${ICONS.PLAY} ` : `${ICONS.PAUSE} ` }), _jsx(Text, { color: theme.colors.primary, bold: true, children: playerState.currentTrack.title }), playerState.currentTrack.artists &&
|
|
184
|
-
playerState.currentTrack.artists.length > 0 && (_jsxs(Text, { color: theme.colors.secondary, children: [' • ', playerState.currentTrack.artists.map(a => a.name).join(', ')] }))] })), _jsxs(Text, { color: theme.colors.dim, children: ["Limit: ", navState.searchLimit, " (Use [ or ] to adjust)"] }), _jsx(SearchBar, { isActive: !editingFilter && isTyping && !isSearching, onInput: input => {
|
|
185
|
-
void performSearch(input);
|
|
186
|
-
} }), editingFilter ? (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.primary, bold: true, children: ["Set ", FILTER_LABELS[editingFilter], " filter:"] }), _jsx(TextInput, { value: filterDraft, onChange: setFilterDraft, onSubmit: handleFilterSubmit, placeholder: "Type value and hit Enter", focus: true })] }), _jsx(Text, { color: theme.colors.dim, children: "Press Enter to save (empty to clear) or Esc to cancel." })] })) : (_jsx(Box, { marginY: 1, children: _jsxs(Text, { color: theme.colors.dim, children: ["Filters: Artist=", artistFilterLabel, ", Album=", albumFilterLabel, ", Year=", yearFilterLabel, ", Duration=", durationFilterLabel, " (Ctrl+A Artist, Ctrl+L Album, Ctrl+Y Year, Ctrl+D Duration)"] }) })), (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: filteredResults, selectedIndex: navState.selectedResult, isActive: !isTyping, onMixCreated: handleMixCreated, onDownloadStatus: handleDownloadStatus })), !isLoading &&
|
|
187
|
-
navState.hasSearched &&
|
|
188
|
-
filteredResults.length === 0 &&
|
|
189
|
-
!error && _jsx(Text, { color: theme.colors.dim, children: "No results found" }), actionMessage && (_jsx(Text, { color: theme.colors.accent, children: actionMessage })), _jsx(Text, { color: theme.colors.dim, children: isTyping
|
|
190
|
-
? 'Type to search, Enter to start, Esc to clear'
|
|
191
|
-
: `Arrows to navigate, Enter to play, M mix, Shift+D download, ]/[ more/fewer results (${navState.searchLimit}), H history, Esc to type` })] }));
|
|
192
|
-
}
|
|
193
|
-
export default React.memo(SearchLayout);
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// Trending tracks view — shows YouTube trending music
|
|
3
|
-
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import { useState, useEffect } from 'react';
|
|
5
|
-
import { useTheme } from "../../hooks/useTheme.js";
|
|
6
|
-
import { useNavigation } from "../../hooks/useNavigation.js";
|
|
7
|
-
import { usePlayer } from "../../hooks/usePlayer.js";
|
|
8
|
-
import { getMusicService } from "../../services/youtube-music/api.js";
|
|
9
|
-
export default function TrendingLayout() {
|
|
10
|
-
const { theme } = useTheme();
|
|
11
|
-
const { dispatch } = useNavigation();
|
|
12
|
-
const { play } = usePlayer();
|
|
13
|
-
const [tracks, setTracks] = useState([]);
|
|
14
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
15
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
16
|
-
const [error, setError] = useState(null);
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
let cancelled = false;
|
|
19
|
-
getMusicService()
|
|
20
|
-
.getTrending()
|
|
21
|
-
.then(results => {
|
|
22
|
-
if (!cancelled) {
|
|
23
|
-
setTracks(results);
|
|
24
|
-
setIsLoading(false);
|
|
25
|
-
}
|
|
26
|
-
})
|
|
27
|
-
.catch((err) => {
|
|
28
|
-
if (!cancelled) {
|
|
29
|
-
setError(err instanceof Error ? err.message : 'Failed to load trending');
|
|
30
|
-
setIsLoading(false);
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
return () => {
|
|
34
|
-
cancelled = true;
|
|
35
|
-
};
|
|
36
|
-
}, []);
|
|
37
|
-
useInput((input, key) => {
|
|
38
|
-
if (key.escape) {
|
|
39
|
-
dispatch({ category: 'GO_BACK' });
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
if (key.upArrow || input === 'k') {
|
|
43
|
-
setSelectedIndex(i => Math.max(0, i - 1));
|
|
44
|
-
}
|
|
45
|
-
else if (key.downArrow || input === 'j') {
|
|
46
|
-
setSelectedIndex(i => Math.min(tracks.length - 1, i + 1));
|
|
47
|
-
}
|
|
48
|
-
else if (key.return) {
|
|
49
|
-
const track = tracks[selectedIndex];
|
|
50
|
-
if (track)
|
|
51
|
-
play(track);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.colors.primary, bold: true, children: "\uD83D\uDD25 Trending Music" }) }), isLoading ? (_jsx(Text, { color: theme.colors.dim, children: "Loading trending tracks..." })) : error ? (_jsx(Text, { color: theme.colors.error, children: error })) : tracks.length === 0 ? (_jsx(Text, { color: theme.colors.dim, children: "No trending tracks found" })) : (tracks.map((track, index) => {
|
|
55
|
-
const isSelected = index === selectedIndex;
|
|
56
|
-
const artist = track.artists?.[0]?.name ?? 'Unknown';
|
|
57
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? theme.colors.primary : theme.colors.dim, children: isSelected ? '▶ ' : `${String(index + 1).padStart(2)}. ` }), _jsx(Text, { color: isSelected ? theme.colors.primary : theme.colors.text, bold: isSelected, children: track.title }), _jsxs(Text, { color: theme.colors.dim, children: [" \u2014 ", artist] })] }, track.videoId));
|
|
58
|
-
})), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.dim, children: "\u2191/\u2193 Navigate | Enter Play | Esc Back" }) })] }));
|
|
59
|
-
}
|