@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,84 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// Custom keybindings editor — shows all actions and their bound keys
|
|
3
|
-
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import { useState } from 'react';
|
|
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 { KEYBINDINGS } from "../../utils/constants.js";
|
|
9
|
-
function buildEntries() {
|
|
10
|
-
const config = getConfigService();
|
|
11
|
-
return Object.entries(KEYBINDINGS).map(([action, defaultKeys]) => {
|
|
12
|
-
const custom = config.getKeybinding(action);
|
|
13
|
-
return {
|
|
14
|
-
action,
|
|
15
|
-
label: action
|
|
16
|
-
.toLowerCase()
|
|
17
|
-
.replace(/_/g, ' ')
|
|
18
|
-
.replace(/\b\w/g, c => c.toUpperCase()),
|
|
19
|
-
keys: custom ?? [...defaultKeys],
|
|
20
|
-
};
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
export default function KeybindingsLayout() {
|
|
24
|
-
const { theme } = useTheme();
|
|
25
|
-
const { dispatch } = useNavigation();
|
|
26
|
-
const [entries, setEntries] = useState(buildEntries);
|
|
27
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
28
|
-
const [isCapturing, setIsCapturing] = useState(false);
|
|
29
|
-
const [statusMessage, setStatusMessage] = useState('');
|
|
30
|
-
useInput((input, key) => {
|
|
31
|
-
if (isCapturing) {
|
|
32
|
-
// Build key string from the pressed key
|
|
33
|
-
const parts = [];
|
|
34
|
-
if (key.ctrl)
|
|
35
|
-
parts.push('ctrl');
|
|
36
|
-
if (key.meta)
|
|
37
|
-
parts.push('meta');
|
|
38
|
-
if (key.shift)
|
|
39
|
-
parts.push('shift');
|
|
40
|
-
let keyName = input;
|
|
41
|
-
if (key.upArrow)
|
|
42
|
-
keyName = 'up';
|
|
43
|
-
else if (key.downArrow)
|
|
44
|
-
keyName = 'down';
|
|
45
|
-
else if (key.leftArrow)
|
|
46
|
-
keyName = 'left';
|
|
47
|
-
else if (key.rightArrow)
|
|
48
|
-
keyName = 'right';
|
|
49
|
-
else if (key.return)
|
|
50
|
-
keyName = 'enter';
|
|
51
|
-
else if (key.tab)
|
|
52
|
-
keyName = 'tab';
|
|
53
|
-
else if (key.backspace || key.delete)
|
|
54
|
-
keyName = 'backspace';
|
|
55
|
-
else if (key.escape) {
|
|
56
|
-
setIsCapturing(false);
|
|
57
|
-
setStatusMessage('Cancelled');
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
if (!keyName || keyName.length === 0)
|
|
61
|
-
return;
|
|
62
|
-
parts.push(keyName);
|
|
63
|
-
const newKey = parts.join('+');
|
|
64
|
-
// Persist new binding
|
|
65
|
-
const entry = entries[selectedIndex];
|
|
66
|
-
if (!entry)
|
|
67
|
-
return;
|
|
68
|
-
getConfigService().setKeybinding(entry.action, [newKey]);
|
|
69
|
-
setEntries(buildEntries());
|
|
70
|
-
setIsCapturing(false);
|
|
71
|
-
setStatusMessage(`Bound ${entry.action} to "${newKey}"`);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (key.escape) {
|
|
75
|
-
dispatch({ category: 'GO_BACK' });
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
if (key.upArrow || input === 'k') {
|
|
79
|
-
setSelectedIndex(i => Math.max(0, i - 1));
|
|
80
|
-
}
|
|
81
|
-
else if (key.downArrow || input === 'j') {
|
|
82
|
-
setSelectedIndex(i => Math.min(entries.length - 1, i + 1));
|
|
83
|
-
}
|
|
84
|
-
else if (key.return) {
|
|
85
|
-
setIsCapturing(true);
|
|
86
|
-
setStatusMessage('Press any key to bind...');
|
|
87
|
-
}
|
|
88
|
-
else if (input === 'r') {
|
|
89
|
-
// Reset selected binding to default
|
|
90
|
-
const entry = entries[selectedIndex];
|
|
91
|
-
if (!entry)
|
|
92
|
-
return;
|
|
93
|
-
const defaultKeys = KEYBINDINGS[entry.action];
|
|
94
|
-
if (defaultKeys) {
|
|
95
|
-
getConfigService().setKeybinding(entry.action, [
|
|
96
|
-
...defaultKeys,
|
|
97
|
-
]);
|
|
98
|
-
setEntries(buildEntries());
|
|
99
|
-
setStatusMessage(`Reset ${entry.action} to default`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.colors.primary, bold: true, children: "Custom Keybindings" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.colors.dim, children: "\u2191/\u2193 Navigate | Enter Edit | r Reset | Esc Back" }) }), statusMessage ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.colors.secondary, children: statusMessage }) })) : null, entries.map((entry, index) => {
|
|
104
|
-
const isSelected = index === selectedIndex;
|
|
105
|
-
return (_jsxs(Box, { marginBottom: 0, children: [_jsx(Text, { color: isSelected ? theme.colors.primary : theme.colors.text, bold: isSelected, children: isSelected ? '▶ ' : ' ' }), _jsx(Text, { color: isSelected ? theme.colors.primary : theme.colors.text, bold: isSelected, children: entry.label.padEnd(25) }), _jsx(Text, { color: theme.colors.secondary, children: entry.keys.join(', ') })] }, entry.action));
|
|
106
|
-
}), isCapturing ? (_jsxs(Box, { marginTop: 1, borderStyle: "single", borderColor: theme.colors.secondary, padding: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: ["Press any key combination...", ' '] }), _jsx(Text, { color: theme.colors.dim, children: "(Esc to cancel)" })] })) : null] }));
|
|
107
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
// Export layout component for playlist export
|
|
3
|
-
import { useState, useCallback, useMemo } from 'react';
|
|
4
|
-
import { Box, Text } from 'ink';
|
|
5
|
-
import { useTheme } from "../../hooks/useTheme.js";
|
|
6
|
-
import { useNavigation } from "../../hooks/useNavigation.js";
|
|
7
|
-
import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
8
|
-
import { KEYBINDINGS } from "../../utils/constants.js";
|
|
9
|
-
import { getExportService, } from "../../services/export/export.service.js";
|
|
10
|
-
import { getConfigService } from "../../services/config/config.service.js";
|
|
11
|
-
const FORMATS = [
|
|
12
|
-
{ key: 'json', label: 'JSON' },
|
|
13
|
-
{ key: 'm3u8', label: 'M3U8' },
|
|
14
|
-
{ key: 'both', label: 'Both (JSON + M3U8)' },
|
|
15
|
-
];
|
|
16
|
-
export default function ExportLayout() {
|
|
17
|
-
const { theme } = useTheme();
|
|
18
|
-
const { dispatch } = useNavigation();
|
|
19
|
-
const config = getConfigService();
|
|
20
|
-
const exportService = getExportService();
|
|
21
|
-
const [step, setStep] = useState('format');
|
|
22
|
-
const [selectedFormat, setSelectedFormat] = useState(2); // Default to 'both'
|
|
23
|
-
const [selectedPlaylist, setSelectedPlaylist] = useState(-1); // -1 means "Export All"
|
|
24
|
-
const [results, setResults] = useState([]);
|
|
25
|
-
const playlists = useMemo(() => config.get('playlists') || [], [config]);
|
|
26
|
-
const goBack = useCallback(() => {
|
|
27
|
-
if (step === 'format') {
|
|
28
|
-
dispatch({ category: 'GO_BACK' });
|
|
29
|
-
}
|
|
30
|
-
else if (step === 'playlist') {
|
|
31
|
-
setStep('format');
|
|
32
|
-
}
|
|
33
|
-
else if (step === 'result') {
|
|
34
|
-
setStep('format');
|
|
35
|
-
setResults([]);
|
|
36
|
-
}
|
|
37
|
-
}, [step, dispatch]);
|
|
38
|
-
const selectFormat = useCallback(() => {
|
|
39
|
-
setStep('playlist');
|
|
40
|
-
}, []);
|
|
41
|
-
const startExport = useCallback(async () => {
|
|
42
|
-
setStep('exporting');
|
|
43
|
-
const format = FORMATS[selectedFormat].key;
|
|
44
|
-
try {
|
|
45
|
-
if (selectedPlaylist === -1) {
|
|
46
|
-
// Export all playlists
|
|
47
|
-
const exportResults = await exportService.exportAllPlaylists(playlists, {
|
|
48
|
-
format,
|
|
49
|
-
});
|
|
50
|
-
setResults(exportResults);
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
// Export single playlist
|
|
54
|
-
const result = await exportService.exportPlaylist(playlists[selectedPlaylist], {
|
|
55
|
-
format,
|
|
56
|
-
});
|
|
57
|
-
setResults([result]);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
setResults([
|
|
62
|
-
{
|
|
63
|
-
playlistName: 'Error',
|
|
64
|
-
success: false,
|
|
65
|
-
files: [],
|
|
66
|
-
error: error instanceof Error ? error.message : String(error),
|
|
67
|
-
},
|
|
68
|
-
]);
|
|
69
|
-
}
|
|
70
|
-
finally {
|
|
71
|
-
setStep('result');
|
|
72
|
-
}
|
|
73
|
-
}, [selectedFormat, selectedPlaylist, playlists, exportService]);
|
|
74
|
-
// Keyboard bindings
|
|
75
|
-
useKeyBinding(KEYBINDINGS.UP, () => {
|
|
76
|
-
if (step === 'format') {
|
|
77
|
-
setSelectedFormat(prev => Math.max(0, prev - 1));
|
|
78
|
-
}
|
|
79
|
-
else if (step === 'playlist') {
|
|
80
|
-
setSelectedPlaylist(prev => Math.max(-1, prev - 1));
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
useKeyBinding(KEYBINDINGS.DOWN, () => {
|
|
84
|
-
if (step === 'format') {
|
|
85
|
-
setSelectedFormat(prev => Math.min(FORMATS.length - 1, prev + 1));
|
|
86
|
-
}
|
|
87
|
-
else if (step === 'playlist') {
|
|
88
|
-
setSelectedPlaylist(prev => Math.min(playlists.length - 1, prev === -1 && playlists.length > 0 ? 0 : prev + 1));
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
useKeyBinding(KEYBINDINGS.SELECT, () => {
|
|
92
|
-
if (step === 'format')
|
|
93
|
-
selectFormat();
|
|
94
|
-
else if (step === 'playlist')
|
|
95
|
-
startExport();
|
|
96
|
-
else if (step === 'result')
|
|
97
|
-
goBack();
|
|
98
|
-
});
|
|
99
|
-
useKeyBinding(KEYBINDINGS.BACK, goBack);
|
|
100
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 1, children: [_jsx(Box, { borderStyle: "double", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Export Playlists" }) }), step === 'format' && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: theme.colors.dim, children: "Select export format:" }), FORMATS.map((format, index) => (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: index === selectedFormat ? theme.colors.primary : undefined, color: index === selectedFormat
|
|
101
|
-
? theme.colors.background
|
|
102
|
-
: theme.colors.text, bold: index === selectedFormat, children: [index === selectedFormat ? '► ' : ' ', format.label] }) }, format.key))), playlists.length === 0 && (_jsx(Box, { marginTop: 1, paddingX: 1, children: _jsx(Text, { color: theme.colors.error, children: "No playlists found. Create or import playlists first." }) }))] })), step === 'playlist' && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: theme.colors.dim, children: "Select playlist to export:" }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedPlaylist === -1 ? theme.colors.primary : undefined, color: selectedPlaylist === -1
|
|
103
|
-
? theme.colors.background
|
|
104
|
-
: theme.colors.text, bold: selectedPlaylist === -1, children: [selectedPlaylist === -1 ? '► ' : ' ', "Export All (", playlists.length, " playlists)"] }) }), playlists.map((playlist, index) => (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: index === selectedPlaylist ? theme.colors.primary : undefined, color: index === selectedPlaylist
|
|
105
|
-
? theme.colors.background
|
|
106
|
-
: theme.colors.text, bold: index === selectedPlaylist, children: [index === selectedPlaylist ? '► ' : ' ', playlist.name, " (", playlist.tracks.length, " tracks)"] }) }, playlist.playlistId)))] })), step === 'exporting' && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: theme.colors.primary, children: "Exporting..." }), _jsx(Text, { color: theme.colors.dim, children: selectedPlaylist === -1
|
|
107
|
-
? `Exporting ${playlists.length} playlists...`
|
|
108
|
-
: `Exporting ${playlists[selectedPlaylist]?.name || 'playlist'}...` })] })), step === 'result' && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.colors.success, bold: true, children: "Export completed!" }) }), results.map((result, index) => (_jsx(Box, { flexDirection: "column", gap: 1, paddingX: 1, children: result.success ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.colors.success, children: ["\u2713 ", result.playlistName] }), result.files.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [_jsx(Text, { color: theme.colors.dim, children: "Exported to:" }), result.files.map(file => (_jsxs(Text, { color: theme.colors.primary, children: ["\u2022 ", file] }, file)))] }))] })) : (_jsxs(Text, { color: theme.colors.error, children: ["\u2717 ", result.playlistName, ": ", result.error] })) }, index))), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsx(Text, { color: theme.colors.dim, children: "Press Enter to continue" }) })] })), step !== 'exporting' && step !== 'result' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.dim, children: step === 'format'
|
|
109
|
-
? '↑↓ to select, Enter to continue, Esc/q to go back'
|
|
110
|
-
: '↑↓ to select, Enter to export, Esc to go back' }) }))] }));
|
|
111
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// Import layout component for playlist import
|
|
3
|
-
import { useState, useCallback, useEffect } from 'react';
|
|
4
|
-
import { Box, Text } from 'ink';
|
|
5
|
-
import TextInput from 'ink-text-input';
|
|
6
|
-
import { useTheme } from "../../hooks/useTheme.js";
|
|
7
|
-
import { useNavigation } from "../../hooks/useNavigation.js";
|
|
8
|
-
import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
9
|
-
import { KEYBINDINGS } from "../../utils/constants.js";
|
|
10
|
-
import { getImportService } from "../../services/import/import.service.js";
|
|
11
|
-
import ImportProgressComponent from "./ImportProgress.js";
|
|
12
|
-
const SOURCES = [
|
|
13
|
-
{ key: 'spotify', label: 'Spotify' },
|
|
14
|
-
{ key: 'youtube', label: 'YouTube' },
|
|
15
|
-
];
|
|
16
|
-
export default function ImportLayout() {
|
|
17
|
-
const { theme } = useTheme();
|
|
18
|
-
const { dispatch } = useNavigation();
|
|
19
|
-
const importService = getImportService();
|
|
20
|
-
const [step, setStep] = useState('source');
|
|
21
|
-
const [selectedSource, setSelectedSource] = useState(0);
|
|
22
|
-
const [url, setUrl] = useState('');
|
|
23
|
-
const [customName, setCustomName] = useState('');
|
|
24
|
-
const [progress, setProgress] = useState(null);
|
|
25
|
-
const [result, setResult] = useState(null);
|
|
26
|
-
const [error, setError] = useState(null);
|
|
27
|
-
const goBack = useCallback(() => {
|
|
28
|
-
if (step === 'source') {
|
|
29
|
-
dispatch({ category: 'GO_BACK' });
|
|
30
|
-
}
|
|
31
|
-
else if (step === 'url') {
|
|
32
|
-
setStep('source');
|
|
33
|
-
}
|
|
34
|
-
else if (step === 'name') {
|
|
35
|
-
setStep('url');
|
|
36
|
-
}
|
|
37
|
-
else if (step === 'result') {
|
|
38
|
-
setStep('source');
|
|
39
|
-
setResult(null);
|
|
40
|
-
}
|
|
41
|
-
}, [step, dispatch]);
|
|
42
|
-
const selectSource = useCallback(() => {
|
|
43
|
-
setStep('url');
|
|
44
|
-
}, []);
|
|
45
|
-
const submitUrl = useCallback(() => {
|
|
46
|
-
if (url.trim()) {
|
|
47
|
-
setStep('name');
|
|
48
|
-
}
|
|
49
|
-
}, [url]);
|
|
50
|
-
const startImport = useCallback(async () => {
|
|
51
|
-
setStep('importing');
|
|
52
|
-
setError(null);
|
|
53
|
-
try {
|
|
54
|
-
const unsubscribe = importService.onProgress(prog => {
|
|
55
|
-
setProgress(prog);
|
|
56
|
-
});
|
|
57
|
-
const source = SOURCES[selectedSource].key;
|
|
58
|
-
const importResult = await importService.importPlaylist(source, url, customName || undefined);
|
|
59
|
-
unsubscribe();
|
|
60
|
-
setResult({
|
|
61
|
-
playlistName: importResult.playlistName,
|
|
62
|
-
matched: importResult.matched,
|
|
63
|
-
total: importResult.total,
|
|
64
|
-
errors: importResult.errors,
|
|
65
|
-
});
|
|
66
|
-
setStep('result');
|
|
67
|
-
}
|
|
68
|
-
catch (err) {
|
|
69
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
70
|
-
setStep('source');
|
|
71
|
-
}
|
|
72
|
-
}, [selectedSource, url, customName, importService]);
|
|
73
|
-
const submitName = useCallback(() => {
|
|
74
|
-
startImport();
|
|
75
|
-
}, [startImport]);
|
|
76
|
-
// Keyboard bindings
|
|
77
|
-
useKeyBinding(KEYBINDINGS.UP, () => {
|
|
78
|
-
if (step === 'source') {
|
|
79
|
-
setSelectedSource(prev => Math.max(0, prev - 1));
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
useKeyBinding(KEYBINDINGS.DOWN, () => {
|
|
83
|
-
if (step === 'source') {
|
|
84
|
-
setSelectedSource(prev => Math.min(SOURCES.length - 1, prev + 1));
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
useKeyBinding(KEYBINDINGS.SELECT, () => {
|
|
88
|
-
if (step === 'source')
|
|
89
|
-
selectSource();
|
|
90
|
-
else if (step === 'url')
|
|
91
|
-
submitUrl();
|
|
92
|
-
else if (step === 'name')
|
|
93
|
-
submitName();
|
|
94
|
-
else if (step === 'result')
|
|
95
|
-
goBack();
|
|
96
|
-
});
|
|
97
|
-
useKeyBinding(KEYBINDINGS.BACK, goBack);
|
|
98
|
-
// Escape key for skip name
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
if (step === 'name') {
|
|
101
|
-
const handleEscape = () => {
|
|
102
|
-
startImport();
|
|
103
|
-
};
|
|
104
|
-
const stdin = process.stdin;
|
|
105
|
-
stdin.on('keypress', handleEscape);
|
|
106
|
-
return () => {
|
|
107
|
-
stdin.off('keypress', handleEscape);
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
return undefined;
|
|
111
|
-
}, [step, startImport]);
|
|
112
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 1, children: [_jsx(Box, { borderStyle: "double", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Import Playlist" }) }), error && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.colors.error, children: ["Error: ", error] }) })), step === 'source' && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: theme.colors.dim, children: "Select playlist source:" }), SOURCES.map((source, index) => (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: index === selectedSource ? theme.colors.primary : undefined, color: index === selectedSource
|
|
113
|
-
? theme.colors.background
|
|
114
|
-
: theme.colors.text, bold: index === selectedSource, children: [index === selectedSource ? '► ' : ' ', source.label] }) }, source.key)))] })), step === 'url' && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { color: theme.colors.dim, children: ["Enter ", SOURCES[selectedSource].label, " playlist URL or ID:"] }), _jsx(Box, { paddingX: 1, children: _jsx(TextInput, { value: url, onChange: setUrl, onSubmit: submitUrl, placeholder: "Paste URL or ID here...", focus: true }) }), _jsxs(Text, { color: theme.colors.dim, children: ["Examples:", ' ', SOURCES[selectedSource].key === 'spotify'
|
|
115
|
-
? 'https://open.spotify.com/playlist/...'
|
|
116
|
-
: 'https://www.youtube.com/playlist?list=...'] })] })), step === 'name' && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: theme.colors.dim, children: "Custom playlist name (optional, Esc to skip):" }), _jsx(Box, { paddingX: 1, children: _jsx(TextInput, { value: customName, onChange: setCustomName, onSubmit: submitName, placeholder: "Leave empty to use original name", focus: true }) }), _jsx(Text, { color: theme.colors.dim, children: "Press Enter to import or Esc to skip" })] })), step === 'importing' && progress && (_jsx(ImportProgressComponent, { progress: progress })), step === 'result' && result && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.colors.success, bold: true, children: "\u2713 Import completed!" }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { children: ["Playlist: ", result.playlistName] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { children: ["Matched:", ' ', _jsx(Text, { color: theme.colors.primary, children: result.matched }), "/", result.total, " tracks"] }) }), result.errors.length > 0 && (_jsxs(Box, { flexDirection: "column", gap: 1, marginTop: 1, children: [_jsxs(Text, { color: theme.colors.dim, bold: true, children: ["Errors (", result.errors.length, "):"] }), result.errors.slice(0, 5).map((err, i) => (_jsx(Box, { paddingX: 2, children: _jsxs(Text, { color: theme.colors.error, children: ["\u2022 ", err] }) }, i))), result.errors.length > 5 && (_jsx(Box, { paddingX: 2, children: _jsxs(Text, { color: theme.colors.dim, children: ["... and ", result.errors.length - 5, " more"] }) }))] })), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsx(Text, { color: theme.colors.dim, children: "Press Enter to continue" }) })] })), step !== 'importing' && step !== 'result' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.dim, children: step === 'source'
|
|
117
|
-
? '↑↓ to select, Enter to continue, Esc/q to go back'
|
|
118
|
-
: 'Enter to continue, Esc to go back' }) }))] }));
|
|
119
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// Import progress display component
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
4
|
-
import { Box, Text } from 'ink';
|
|
5
|
-
import { useTheme } from "../../hooks/useTheme.js";
|
|
6
|
-
const PROGRESS_BLOCKS = 20;
|
|
7
|
-
export default function ImportProgress({ progress }) {
|
|
8
|
-
const { theme } = useTheme();
|
|
9
|
-
const [animatedBlocks, setAnimatedBlocks] = useState(0);
|
|
10
|
-
// Animate progress blocks
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
if (progress.total > 0) {
|
|
13
|
-
const targetBlocks = Math.floor((progress.current / progress.total) * PROGRESS_BLOCKS);
|
|
14
|
-
const delay = Math.max(50, 500 / PROGRESS_BLOCKS); // Max 500ms total animation
|
|
15
|
-
const interval = setInterval(() => {
|
|
16
|
-
setAnimatedBlocks(prev => {
|
|
17
|
-
if (prev >= targetBlocks) {
|
|
18
|
-
clearInterval(interval);
|
|
19
|
-
return prev;
|
|
20
|
-
}
|
|
21
|
-
return prev + 1;
|
|
22
|
-
});
|
|
23
|
-
}, delay);
|
|
24
|
-
return () => clearInterval(interval);
|
|
25
|
-
}
|
|
26
|
-
return undefined;
|
|
27
|
-
}, [progress]);
|
|
28
|
-
// Calculate completed blocks
|
|
29
|
-
const completedBlocks = Math.min(animatedBlocks, PROGRESS_BLOCKS);
|
|
30
|
-
const progressPercent = progress.total > 0
|
|
31
|
-
? Math.round((progress.current / progress.total) * 100)
|
|
32
|
-
: 0;
|
|
33
|
-
// Get status color
|
|
34
|
-
const getStatusColor = () => {
|
|
35
|
-
switch (progress.status) {
|
|
36
|
-
case 'fetching':
|
|
37
|
-
return theme.colors.accent;
|
|
38
|
-
case 'matching':
|
|
39
|
-
return theme.colors.primary;
|
|
40
|
-
case 'creating':
|
|
41
|
-
return theme.colors.success;
|
|
42
|
-
case 'completed':
|
|
43
|
-
return theme.colors.success;
|
|
44
|
-
case 'failed':
|
|
45
|
-
case 'cancelled':
|
|
46
|
-
return theme.colors.error;
|
|
47
|
-
default:
|
|
48
|
-
return theme.colors.dim;
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
// Get status label
|
|
52
|
-
const getStatusLabel = () => {
|
|
53
|
-
switch (progress.status) {
|
|
54
|
-
case 'fetching':
|
|
55
|
-
return 'Fetching playlist...';
|
|
56
|
-
case 'matching':
|
|
57
|
-
return 'Matching tracks...';
|
|
58
|
-
case 'creating':
|
|
59
|
-
return 'Creating playlist...';
|
|
60
|
-
case 'completed':
|
|
61
|
-
return 'Completed!';
|
|
62
|
-
case 'failed':
|
|
63
|
-
return 'Failed';
|
|
64
|
-
case 'cancelled':
|
|
65
|
-
return 'Cancelled';
|
|
66
|
-
default:
|
|
67
|
-
return 'Starting...';
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { paddingX: 1, children: _jsx(Text, { bold: true, color: getStatusColor(), children: getStatusLabel() }) }), progress.total > 0 && (_jsx(Box, { paddingX: 1, children: _jsxs(Box, { children: [Array.from({ length: completedBlocks }).map((_, i) => (_jsx(Text, { backgroundColor: theme.colors.primary, children: ' ' }, i))), Array.from({ length: PROGRESS_BLOCKS - completedBlocks }).map((_, i) => (_jsx(Text, { backgroundColor: theme.colors.dim, dimColor: true, children: ' ' }, i + completedBlocks)))] }) })), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.colors.dim, children: [progress.total > 0 ? `${progressPercent}%` : '...', " -", ' ', progress.current, "/", progress.total || '?', ' ', progress.total === 1 ? 'track' : 'tracks'] }) }), progress.currentTrack && progress.status === 'matching' && (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.colors.text, dimColor: true, children: progress.currentTrack.length > 50
|
|
71
|
-
? `...${progress.currentTrack.slice(-47)}`
|
|
72
|
-
: progress.currentTrack }) })), progress.message && (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.colors.dim, children: progress.message }) }))] }));
|
|
73
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
// Explore / Genre browsing view — shows curated sections from YouTube 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 ExploreLayout() {
|
|
10
|
-
const { theme } = useTheme();
|
|
11
|
-
const { dispatch } = useNavigation();
|
|
12
|
-
const { play } = usePlayer();
|
|
13
|
-
const [sections, setSections] = useState([]);
|
|
14
|
-
const [sectionIndex, setSectionIndex] = useState(0);
|
|
15
|
-
const [trackIndex, setTrackIndex] = useState(0);
|
|
16
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
17
|
-
const [error, setError] = useState(null);
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
let cancelled = false;
|
|
20
|
-
getMusicService()
|
|
21
|
-
.getExploreSections()
|
|
22
|
-
.then(results => {
|
|
23
|
-
if (!cancelled) {
|
|
24
|
-
setSections(results);
|
|
25
|
-
setIsLoading(false);
|
|
26
|
-
}
|
|
27
|
-
})
|
|
28
|
-
.catch((err) => {
|
|
29
|
-
if (!cancelled) {
|
|
30
|
-
setError(err instanceof Error ? err.message : 'Failed to load explore');
|
|
31
|
-
setIsLoading(false);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
return () => {
|
|
35
|
-
cancelled = true;
|
|
36
|
-
};
|
|
37
|
-
}, []);
|
|
38
|
-
const currentSection = sections[sectionIndex];
|
|
39
|
-
const tracks = currentSection?.tracks ?? [];
|
|
40
|
-
useInput((input, key) => {
|
|
41
|
-
if (key.escape) {
|
|
42
|
-
dispatch({ category: 'GO_BACK' });
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
if (key.leftArrow || input === 'h') {
|
|
46
|
-
setSectionIndex(i => Math.max(0, i - 1));
|
|
47
|
-
setTrackIndex(0);
|
|
48
|
-
}
|
|
49
|
-
else if (key.rightArrow || input === 'l') {
|
|
50
|
-
setSectionIndex(i => Math.min(sections.length - 1, i + 1));
|
|
51
|
-
setTrackIndex(0);
|
|
52
|
-
}
|
|
53
|
-
else if (key.upArrow || input === 'k') {
|
|
54
|
-
setTrackIndex(i => Math.max(0, i - 1));
|
|
55
|
-
}
|
|
56
|
-
else if (key.downArrow || input === 'j') {
|
|
57
|
-
setTrackIndex(i => Math.min(tracks.length - 1, i + 1));
|
|
58
|
-
}
|
|
59
|
-
else if (key.return) {
|
|
60
|
-
const track = tracks[trackIndex];
|
|
61
|
-
if (track)
|
|
62
|
-
play(track);
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.colors.primary, bold: true, children: "\uD83C\uDFB5 Explore" }) }), isLoading ? (_jsx(Text, { color: theme.colors.dim, children: "Loading explore sections..." })) : error ? (_jsx(Text, { color: theme.colors.error, children: error })) : sections.length === 0 ? (_jsx(Text, { color: theme.colors.dim, children: "No sections found" })) : (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, gap: 2, children: sections.map((section, index) => (_jsx(Text, { color: index === sectionIndex
|
|
66
|
-
? theme.colors.primary
|
|
67
|
-
: theme.colors.dim, bold: index === sectionIndex, underline: index === sectionIndex, children: section.title }, section.title))) }), tracks.map((track, index) => {
|
|
68
|
-
const isSelected = index === trackIndex;
|
|
69
|
-
const artist = track.artists?.[0]?.name ?? 'Unknown';
|
|
70
|
-
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));
|
|
71
|
-
})] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.dim, children: "\u2190/\u2192 Sections | \u2191/\u2193 Tracks | Enter Play | Esc Back" }) })] }));
|
|
72
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
import { useTheme } from "../../hooks/useTheme.js";
|
|
4
|
-
import { useHistory } from "../../stores/history.store.js";
|
|
5
|
-
import { useTerminalSize } from "../../hooks/useTerminalSize.js";
|
|
6
|
-
import { truncate } from "../../utils/format.js";
|
|
7
|
-
import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
8
|
-
import { KEYBINDINGS } from "../../utils/constants.js";
|
|
9
|
-
import { useNavigation } from "../../hooks/useNavigation.js";
|
|
10
|
-
const DATE_FORMATTER = new Intl.DateTimeFormat(undefined, {
|
|
11
|
-
dateStyle: 'medium',
|
|
12
|
-
timeStyle: 'short',
|
|
13
|
-
});
|
|
14
|
-
function formatTimestamp(iso) {
|
|
15
|
-
const date = new Date(iso);
|
|
16
|
-
if (Number.isNaN(date.getTime())) {
|
|
17
|
-
return iso;
|
|
18
|
-
}
|
|
19
|
-
return DATE_FORMATTER.format(date);
|
|
20
|
-
}
|
|
21
|
-
export default function HistoryLayout() {
|
|
22
|
-
const { theme } = useTheme();
|
|
23
|
-
const { history } = useHistory();
|
|
24
|
-
const { columns } = useTerminalSize();
|
|
25
|
-
const { dispatch } = useNavigation();
|
|
26
|
-
useKeyBinding(KEYBINDINGS.BACK, () => {
|
|
27
|
-
dispatch({ category: 'GO_BACK' });
|
|
28
|
-
});
|
|
29
|
-
const maxTitleLength = Math.max(30, columns - 20);
|
|
30
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, gap: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.colors.primary, bold: true, children: "Recently Played" }) }), history.length === 0 ? (_jsx(Text, { color: theme.colors.dim, children: "No listening history yet." })) : (history.map(entry => {
|
|
31
|
-
const artists = entry.track.artists
|
|
32
|
-
?.map(artist => artist.name)
|
|
33
|
-
.join(', ')
|
|
34
|
-
.trim();
|
|
35
|
-
return (_jsxs(Box, { flexDirection: "column", paddingY: 1, borderStyle: "round", borderColor: theme.colors.dim, children: [_jsx(Text, { color: theme.colors.secondary, children: formatTimestamp(entry.playedAt) }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.text, bold: true, children: truncate(entry.track.title, maxTitleLength) }), _jsx(Text, { color: theme.colors.dim, children: artists ? ` • ${artists}` : '' })] }), entry.track.album?.name && (_jsxs(Text, { color: theme.colors.dim, children: ["Album: ", entry.track.album.name] }))] }, `${entry.playedAt}-${entry.track.videoId}`));
|
|
36
|
-
})), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.dim, children: "Esc to go back \u2022 Shift+H to reopen history" }) })] }));
|
|
37
|
-
}
|