@involvex/youtube-music-cli 0.0.29 → 0.0.31
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/source/components/common/ShortcutsBar.js +4 -3
- package/dist/source/components/layouts/MiniPlayerLayout.js +2 -1
- package/dist/source/components/layouts/SearchLayout.js +2 -1
- package/dist/source/components/player/NowPlaying.js +2 -1
- package/dist/source/components/player/PlayerControls.js +2 -1
- package/dist/source/services/player/player.service.d.ts +4 -1
- package/dist/source/services/player/player.service.js +9 -1
- package/dist/source/stores/player.store.js +41 -8
- package/dist/source/utils/icons.d.ts +19 -0
- package/dist/source/utils/icons.js +26 -0
- package/dist/youtube-music-cli.exe +0 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## [0.0.31](https://github.com/involvex/youtube-music-cli/compare/v0.0.30...v0.0.31) (2026-02-22)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
- improve mpv process management and fix EOF/pause race condition ([b5e9786](https://github.com/involvex/youtube-music-cli/commit/b5e9786d99bdc0dc81cd32bef65ac5467c032231))
|
|
6
|
+
|
|
7
|
+
## [0.0.30](https://github.com/involvex/youtube-music-cli/compare/v0.0.29...v0.0.30) (2026-02-22)
|
|
8
|
+
|
|
1
9
|
## [0.0.29](https://github.com/involvex/youtube-music-cli/compare/v0.0.28...v0.0.29) (2026-02-22)
|
|
2
10
|
|
|
3
11
|
### Bug Fixes
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
// Shortcuts bar component
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import { usePlayer } from "../../hooks/usePlayer.js";
|
|
5
5
|
import { useTheme } from "../../hooks/useTheme.js";
|
|
6
6
|
import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
7
7
|
import { KEYBINDINGS } from "../../utils/constants.js";
|
|
8
|
+
import { ICONS } from "../../utils/icons.js";
|
|
8
9
|
export default function ShortcutsBar() {
|
|
9
10
|
const { theme } = useTheme();
|
|
10
11
|
const { state: playerState, pause, resume, next, previous, volumeUp, volumeDown, volumeFineUp, volumeFineDown, toggleShuffle, toggleRepeat, } = usePlayer();
|
|
@@ -27,7 +28,7 @@ export default function ShortcutsBar() {
|
|
|
27
28
|
useKeyBinding(KEYBINDINGS.SHUFFLE, toggleShuffle);
|
|
28
29
|
useKeyBinding(KEYBINDINGS.REPEAT, toggleRepeat);
|
|
29
30
|
// Note: SETTINGS keybinding handled by MainLayout to avoid double-dispatch
|
|
30
|
-
return (_jsxs(Box, { borderStyle: "single", borderColor: theme.colors.dim, paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: theme.colors.dim, children: [
|
|
31
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: theme.colors.dim, paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: theme.colors.dim, children: [_jsxs(Text, { color: theme.colors.text, children: [ICONS.PLAY_PAUSE_ON, "/", ICONS.PAUSE, " [Space]"] }), ' ', "| ", _jsxs(Text, { color: theme.colors.text, children: [ICONS.PREV, " [B/\u2190]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.NEXT, " [N/\u2192]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.SHUFFLE, " [Shift+S]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.REPEAT_ALL, " [R]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.PLAYLIST, " [Shift+P]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.DOWNLOAD, " [Shift+D]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.SEARCH, " [/]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.HELP, " [?]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.BG_PLAY, " [Shift+Q]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.RESUME, " [Shift+R]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.QUIT, " [Q]"] })] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: playerState.shuffle ? theme.colors.primary : theme.colors.dim, children: ICONS.SHUFFLE }), ' ', _jsx(Text, { color: playerState.repeat === 'off'
|
|
31
32
|
? theme.colors.dim
|
|
32
|
-
: theme.colors.secondary, children: playerState.repeat === 'one' ?
|
|
33
|
+
: theme.colors.secondary, children: playerState.repeat === 'one' ? ICONS.REPEAT_ONE : ICONS.REPEAT_ALL }), ' ', _jsxs(Text, { color: theme.colors.dim, children: [ICONS.VOLUME, " [=/-]"] }), ' ', _jsxs(Text, { color: theme.colors.primary, children: [playerState.volume, "%"] })] })] }));
|
|
33
34
|
}
|
|
@@ -4,6 +4,7 @@ import { Box, Text } from 'ink';
|
|
|
4
4
|
import { usePlayer } from "../../hooks/usePlayer.js";
|
|
5
5
|
import { useTheme } from "../../hooks/useTheme.js";
|
|
6
6
|
import { formatTime } from "../../utils/format.js";
|
|
7
|
+
import { ICONS } from "../../utils/icons.js";
|
|
7
8
|
export default function MiniPlayerLayout() {
|
|
8
9
|
const { theme } = useTheme();
|
|
9
10
|
const { state } = usePlayer();
|
|
@@ -12,7 +13,7 @@ export default function MiniPlayerLayout() {
|
|
|
12
13
|
const title = track?.title ?? 'No track playing';
|
|
13
14
|
const progress = formatTime(state.progress);
|
|
14
15
|
const duration = formatTime(state.duration);
|
|
15
|
-
const playIcon = state.isPlaying ?
|
|
16
|
+
const playIcon = state.isPlaying ? ICONS.PLAY : ICONS.PAUSE;
|
|
16
17
|
const vol = `${state.volume}%`;
|
|
17
18
|
const speed = (state.speed ?? 1.0) !== 1.0 ? ` ${(state.speed ?? 1.0).toFixed(2)}x` : '';
|
|
18
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..." })] }));
|
|
@@ -11,6 +11,7 @@ import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
|
11
11
|
import { KEYBINDINGS, VIEW } from "../../utils/constants.js";
|
|
12
12
|
import { Box, Text } from 'ink';
|
|
13
13
|
import { usePlayer } from "../../hooks/usePlayer.js";
|
|
14
|
+
import { ICONS } from "../../utils/icons.js";
|
|
14
15
|
function SearchLayout() {
|
|
15
16
|
const { theme } = useTheme();
|
|
16
17
|
const { state: navState, dispatch } = useNavigation();
|
|
@@ -118,7 +119,7 @@ function SearchLayout() {
|
|
|
118
119
|
lastAutoSearchedQueryRef.current = null;
|
|
119
120
|
};
|
|
120
121
|
}, [dispatch]);
|
|
121
|
-
return (_jsxs(Box, { flexDirection: "column", children: [playerState.currentTrack && (_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.dim, children: playerState.isPlaying ?
|
|
122
|
+
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 &&
|
|
122
123
|
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: isTyping && !isSearching, onInput: input => {
|
|
123
124
|
void performSearch(input);
|
|
124
125
|
} }), (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, onMixCreated: handleMixCreated, onDownloadStatus: handleDownloadStatus })), !isLoading && navState.hasSearched && results.length === 0 && !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
|
|
@@ -7,6 +7,7 @@ import { formatTime } from "../../utils/format.js";
|
|
|
7
7
|
import { useTerminalSize } from "../../hooks/useTerminalSize.js";
|
|
8
8
|
import { getSleepTimerService } from "../../services/sleep-timer/sleep-timer.service.js";
|
|
9
9
|
import { useState, useEffect } from 'react';
|
|
10
|
+
import { ICONS } from "../../utils/icons.js";
|
|
10
11
|
export default function NowPlaying() {
|
|
11
12
|
const { theme } = useTheme();
|
|
12
13
|
const { state: playerState } = usePlayer();
|
|
@@ -40,5 +41,5 @@ export default function NowPlaying() {
|
|
|
40
41
|
const percentage = duration > 0 ? Math.min(100, Math.floor((progress / duration) * 100)) : 0;
|
|
41
42
|
const barWidth = Math.max(10, columns - 8);
|
|
42
43
|
const filledWidth = duration > 0 ? Math.floor((progress / duration) * barWidth) : 0;
|
|
43
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.colors.primary, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: track.title }), _jsx(Text, { color: theme.colors.dim, children: " \u2022 " }), _jsx(Text, { color: theme.colors.secondary, children: artists })] }), track.album && _jsx(Text, { color: theme.colors.dim, children: track.album.name }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.primary, children: '█'.repeat(Math.min(filledWidth, barWidth)) }), _jsx(Text, { color: theme.colors.dim, children: '░'.repeat(Math.max(0, barWidth - filledWidth)) })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.text, children: formatTime(progress) }), _jsxs(Text, { color: theme.colors.dim, children: [" / ", formatTime(duration), " "] }), _jsxs(Text, { color: theme.colors.dim, children: ["[", percentage, "%]"] }), playerState.isLoading && (_jsx(Text, { color: theme.colors.accent, children: " Loading..." })), !playerState.isPlaying && progress > 0 && (
|
|
44
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.colors.primary, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: track.title }), _jsx(Text, { color: theme.colors.dim, children: " \u2022 " }), _jsx(Text, { color: theme.colors.secondary, children: artists })] }), track.album && _jsx(Text, { color: theme.colors.dim, children: track.album.name }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.primary, children: '█'.repeat(Math.min(filledWidth, barWidth)) }), _jsx(Text, { color: theme.colors.dim, children: '░'.repeat(Math.max(0, barWidth - filledWidth)) })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.text, children: formatTime(progress) }), _jsxs(Text, { color: theme.colors.dim, children: [" / ", formatTime(duration), " "] }), _jsxs(Text, { color: theme.colors.dim, children: ["[", percentage, "%]"] }), playerState.isLoading && (_jsx(Text, { color: theme.colors.accent, children: " Loading..." })), !playerState.isPlaying && progress > 0 && (_jsxs(Text, { color: theme.colors.dim, children: [" ", ICONS.PAUSE] })), playerState.shuffle && (_jsxs(Text, { color: theme.colors.primary, children: [" ", ICONS.SHUFFLE] })), sleepRemaining !== null && (_jsxs(Text, { color: theme.colors.warning, children: [' ', "\u23FE ", formatTime(sleepRemaining)] }))] }), playerState.error && (_jsx(Text, { color: theme.colors.error, children: playerState.error }))] }));
|
|
44
45
|
}
|
|
@@ -7,6 +7,7 @@ import { useTheme } from "../../hooks/useTheme.js";
|
|
|
7
7
|
import { Box, Text } from 'ink';
|
|
8
8
|
import { useEffect } from 'react';
|
|
9
9
|
import { logger } from "../../services/logger/logger.service.js";
|
|
10
|
+
import { ICONS } from "../../utils/icons.js";
|
|
10
11
|
let mountCount = 0;
|
|
11
12
|
export default function PlayerControls() {
|
|
12
13
|
const instanceId = ++mountCount;
|
|
@@ -40,5 +41,5 @@ export default function PlayerControls() {
|
|
|
40
41
|
useKeyBinding(KEYBINDINGS.SPEED_UP, speedUp);
|
|
41
42
|
useKeyBinding(KEYBINDINGS.SPEED_DOWN, speedDown);
|
|
42
43
|
useKeyBinding(KEYBINDINGS.SHUFFLE, toggleShuffle);
|
|
43
|
-
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: "\u2190 / b" }), "] Prev"] }), _jsx(Text, { color: theme.colors.primary, children: playerState.isPlaying ? (_jsxs(Text, { children: ["[", _jsx(Text, { color: theme.colors.dim, children: "Space" }), "] Pause"] })) : (_jsxs(Text, { children: ["[", _jsx(Text, { color: theme.colors.dim, children: "Space" }), "] Play"] })) }), _jsxs(Text, { color: theme.colors.text, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "\u2192 / n" }), "] Next"] }), _jsxs(Text, { color: theme.colors.text, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "+/-" }), "] Vol: ", playerState.volume, "%"] }), _jsxs(Text, { color: playerState.shuffle ? theme.colors.primary : theme.colors.dim, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "Shift+S" }), "]", ' ', playerState.shuffle ?
|
|
44
|
+
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: "\u2190 / b" }), "] Prev"] }), _jsx(Text, { color: theme.colors.primary, children: playerState.isPlaying ? (_jsxs(Text, { children: ["[", _jsx(Text, { color: theme.colors.dim, children: "Space" }), "] Pause"] })) : (_jsxs(Text, { children: ["[", _jsx(Text, { color: theme.colors.dim, children: "Space" }), "] Play"] })) }), _jsxs(Text, { color: theme.colors.text, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "\u2192 / n" }), "] Next"] }), _jsxs(Text, { color: theme.colors.text, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "+/-" }), "] Vol: ", playerState.volume, "%"] }), _jsxs(Text, { color: playerState.shuffle ? theme.colors.primary : theme.colors.dim, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "Shift+S" }), "]", ' ', playerState.shuffle ? `${ICONS.SHUFFLE} ON` : `${ICONS.SHUFFLE} OFF`] }), (playerState.speed ?? 1.0) !== 1.0 && (_jsxs(Text, { color: theme.colors.accent, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "<>" }), "]", ' ', (playerState.speed ?? 1.0).toFixed(2), "x"] }))] }));
|
|
44
45
|
}
|
|
@@ -65,7 +65,10 @@ declare class PlayerService {
|
|
|
65
65
|
/**
|
|
66
66
|
* Reattach to an existing mpv process via IPC
|
|
67
67
|
*/
|
|
68
|
-
reattach(ipcPath: string
|
|
68
|
+
reattach(ipcPath: string, options?: {
|
|
69
|
+
trackId?: string;
|
|
70
|
+
currentUrl?: string;
|
|
71
|
+
}): Promise<void>;
|
|
69
72
|
setVolume(volume: number): void;
|
|
70
73
|
getVolume(): number;
|
|
71
74
|
setSpeed(speed: number): void;
|
|
@@ -378,6 +378,10 @@ class PlayerService {
|
|
|
378
378
|
currentUrl: this.currentUrl,
|
|
379
379
|
};
|
|
380
380
|
if (this.mpvProcess) {
|
|
381
|
+
// Close piped stdio handles so Node has no open references that could
|
|
382
|
+
// prevent clean exit or send SIGHUP to the detached mpv process.
|
|
383
|
+
this.mpvProcess.stdout?.destroy();
|
|
384
|
+
this.mpvProcess.stderr?.destroy();
|
|
381
385
|
// Allow detached mpv process to survive after CLI exits.
|
|
382
386
|
this.mpvProcess.unref();
|
|
383
387
|
}
|
|
@@ -391,11 +395,15 @@ class PlayerService {
|
|
|
391
395
|
/**
|
|
392
396
|
* Reattach to an existing mpv process via IPC
|
|
393
397
|
*/
|
|
394
|
-
async reattach(ipcPath) {
|
|
398
|
+
async reattach(ipcPath, options) {
|
|
395
399
|
logger.info('PlayerService', 'Reattaching to player', { ipcPath });
|
|
396
400
|
this.ipcPath = ipcPath;
|
|
397
401
|
await this.connectIpc();
|
|
398
402
|
this.isPlaying = true;
|
|
403
|
+
if (options?.trackId)
|
|
404
|
+
this.currentTrackId = options.trackId;
|
|
405
|
+
if (options?.currentUrl)
|
|
406
|
+
this.currentUrl = options.currentUrl;
|
|
399
407
|
logger.info('PlayerService', 'Successfully reattached to player');
|
|
400
408
|
}
|
|
401
409
|
setVolume(volume) {
|
|
@@ -241,6 +241,7 @@ function PlayerManager() {
|
|
|
241
241
|
});
|
|
242
242
|
}, [dispatch]);
|
|
243
243
|
// Register event handler for mpv IPC events
|
|
244
|
+
const eofTimestampRef = useRef(0);
|
|
244
245
|
useEffect(() => {
|
|
245
246
|
let lastProgressUpdate = 0;
|
|
246
247
|
const PROGRESS_THROTTLE_MS = 1000; // Update progress max once per second
|
|
@@ -256,7 +257,19 @@ function PlayerManager() {
|
|
|
256
257
|
lastProgressUpdate = now;
|
|
257
258
|
}
|
|
258
259
|
}
|
|
260
|
+
if (event.eof) {
|
|
261
|
+
// Track ended — record timestamp so we can suppress mpv's spurious
|
|
262
|
+
// pause event that immediately follows EOF (idle state).
|
|
263
|
+
eofTimestampRef.current = Date.now();
|
|
264
|
+
next();
|
|
265
|
+
}
|
|
259
266
|
if (event.paused !== undefined) {
|
|
267
|
+
// mpv sends pause=true when a track ends and it enters idle mode.
|
|
268
|
+
// Suppress this for ~2s after EOF to prevent it from overwriting
|
|
269
|
+
// the isPlaying:true set by NEXT, which would block autoplay.
|
|
270
|
+
if (event.paused && Date.now() - eofTimestampRef.current < 2000) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
260
273
|
if (event.paused) {
|
|
261
274
|
dispatch({ category: 'PAUSE' });
|
|
262
275
|
}
|
|
@@ -264,10 +277,6 @@ function PlayerManager() {
|
|
|
264
277
|
dispatch({ category: 'RESUME' });
|
|
265
278
|
}
|
|
266
279
|
}
|
|
267
|
-
if (event.eof) {
|
|
268
|
-
// Track ended, play next
|
|
269
|
-
next();
|
|
270
|
-
}
|
|
271
280
|
});
|
|
272
281
|
}, [playerService, dispatch, next]);
|
|
273
282
|
// Initialize audio on mount
|
|
@@ -310,6 +319,32 @@ function PlayerManager() {
|
|
|
310
319
|
videoId: track.videoId,
|
|
311
320
|
});
|
|
312
321
|
const loadAndPlayTrack = async () => {
|
|
322
|
+
// If a detached background session exists for this exact track, reattach
|
|
323
|
+
// to the still-running mpv process instead of spawning a new one.
|
|
324
|
+
const config = getConfigService();
|
|
325
|
+
const bgState = config.getBackgroundPlaybackState();
|
|
326
|
+
const trackUrl = `https://www.youtube.com/watch?v=${track.videoId}`;
|
|
327
|
+
if (bgState.enabled &&
|
|
328
|
+
bgState.ipcPath &&
|
|
329
|
+
bgState.currentUrl === trackUrl) {
|
|
330
|
+
try {
|
|
331
|
+
await playerService.reattach(bgState.ipcPath, {
|
|
332
|
+
trackId: track.videoId,
|
|
333
|
+
currentUrl: trackUrl,
|
|
334
|
+
});
|
|
335
|
+
config.clearBackgroundPlaybackState();
|
|
336
|
+
dispatch({ category: 'SET_LOADING', loading: false });
|
|
337
|
+
logger.info('PlayerManager', 'Reattached to background mpv session');
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
logger.warn('PlayerManager', 'Failed to reattach background session, starting fresh', {
|
|
342
|
+
error: error instanceof Error ? error.message : String(error),
|
|
343
|
+
});
|
|
344
|
+
config.clearBackgroundPlaybackState();
|
|
345
|
+
// Fall through to normal play()
|
|
346
|
+
}
|
|
347
|
+
}
|
|
313
348
|
dispatch({ category: 'SET_LOADING', loading: true });
|
|
314
349
|
const MAX_RETRIES = 3;
|
|
315
350
|
const RETRY_DELAY_MS = 1500;
|
|
@@ -452,11 +487,9 @@ function PlayerManager() {
|
|
|
452
487
|
if (state.repeat === 'one') {
|
|
453
488
|
dispatch({ category: 'SEEK', position: 0 });
|
|
454
489
|
}
|
|
455
|
-
|
|
456
|
-
next();
|
|
457
|
-
}
|
|
490
|
+
// next() for regular track completion is handled by the eof IPC event
|
|
458
491
|
}
|
|
459
|
-
}, [state.progress, state.duration, state.repeat,
|
|
492
|
+
}, [state.progress, state.duration, state.repeat, dispatch]);
|
|
460
493
|
return null;
|
|
461
494
|
}
|
|
462
495
|
export function PlayerProvider({ children }) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const ICONS: {
|
|
2
|
+
readonly PLAY: "▶";
|
|
3
|
+
readonly PAUSE: "‖";
|
|
4
|
+
readonly PLAY_PAUSE_ON: "▶";
|
|
5
|
+
readonly PLAY_PAUSE_OFF: "‖";
|
|
6
|
+
readonly NEXT: "▶|";
|
|
7
|
+
readonly PREV: "|◀";
|
|
8
|
+
readonly SHUFFLE: "⇄";
|
|
9
|
+
readonly REPEAT_ALL: "↻";
|
|
10
|
+
readonly REPEAT_ONE: "↺";
|
|
11
|
+
readonly PLAYLIST: "☰";
|
|
12
|
+
readonly SEARCH: "/";
|
|
13
|
+
readonly HELP: "?";
|
|
14
|
+
readonly DOWNLOAD: "↓";
|
|
15
|
+
readonly QUIT: "×";
|
|
16
|
+
readonly RESUME: "⟳";
|
|
17
|
+
readonly BG_PLAY: "○";
|
|
18
|
+
readonly VOLUME: "♪";
|
|
19
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Universal icon constants using widely-supported Unicode BMP characters.
|
|
2
|
+
// No emoji, no Nerd Font codepoints — renders correctly on any terminal font.
|
|
3
|
+
export const ICONS = {
|
|
4
|
+
// Playback controls
|
|
5
|
+
PLAY: '▶', // U+25B6
|
|
6
|
+
PAUSE: '‖', // U+2016
|
|
7
|
+
PLAY_PAUSE_ON: '▶', // when playing
|
|
8
|
+
PLAY_PAUSE_OFF: '‖', // when paused
|
|
9
|
+
NEXT: '▶|', // next track
|
|
10
|
+
PREV: '|◀', // previous track
|
|
11
|
+
// Playback modes
|
|
12
|
+
SHUFFLE: '⇄', // U+21C4
|
|
13
|
+
REPEAT_ALL: '↻', // U+21BB
|
|
14
|
+
REPEAT_ONE: '↺', // U+21BA
|
|
15
|
+
// Navigation / views
|
|
16
|
+
PLAYLIST: '☰', // U+2630
|
|
17
|
+
SEARCH: '/', // ASCII
|
|
18
|
+
HELP: '?', // ASCII
|
|
19
|
+
// Actions
|
|
20
|
+
DOWNLOAD: '↓', // U+2193
|
|
21
|
+
QUIT: '×', // U+00D7
|
|
22
|
+
RESUME: '⟳', // U+27F3
|
|
23
|
+
BG_PLAY: '○', // U+25CB
|
|
24
|
+
// Status
|
|
25
|
+
VOLUME: '♪', // U+266A
|
|
26
|
+
};
|
|
Binary file
|