@ciphore/radiocli 0.2.2 → 0.2.3
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 +47 -1
- package/README.md +30 -0
- package/dist/agent/alarm-service.js +210 -0
- package/dist/agent/cli.js +193 -0
- package/dist/agent/headless-host.js +143 -0
- package/dist/agent/launcher.js +71 -0
- package/dist/agent/mcp-install.js +467 -0
- package/dist/agent/mcp-server.js +139 -0
- package/dist/agent/service.js +347 -0
- package/dist/agent/session.js +248 -0
- package/dist/alarms/cli.js +4 -1
- package/dist/alarms/runner.js +43 -26
- package/dist/cli.js +60 -3
- package/dist/player/player-controller.js +19 -0
- package/dist/providers/provider-manager.js +5 -0
- package/dist/providers/radio-browser.js +4 -0
- package/dist/setup.js +71 -2
- package/dist/storage/store.js +33 -1
- package/dist/types.js +6 -0
- package/dist/ui/AdaptiveContent.js +24 -9
- package/dist/ui/App.js +299 -18
- package/dist/ui/AppContent.js +4 -4
- package/dist/ui/components/StationList.js +3 -5
- package/dist/ui/components/VersionIndicator.js +19 -0
- package/dist/ui/page-footer.js +4 -2
- package/dist/ui/screen-items.js +40 -9
- package/dist/ui/screens/AlarmsScreen.js +2 -1
- package/dist/ui/screens/CountriesScreen.js +8 -5
- package/dist/ui/screens/HomeScreen.js +3 -1
- package/dist/ui/screens/SettingsScreen.js +70 -53
- package/dist/ui/use-alarm-tui.js +4 -0
- package/dist/ui/use-app-input.js +29 -3
- package/dist/ui/visualizers/gallop.js +118 -0
- package/dist/ui/visualizers/horse-stride.js +20 -0
- package/dist/ui/visualizers/receiver-style-registry.js +12 -2
- package/dist/ui/visualizers/receiver-visualizers.js +3 -0
- package/dist/ui/visualizers/retro-receivers.js +4 -0
- package/dist/ui/visualizers/terminal-receivers.js +57 -0
- package/dist/update-check.js +26 -7
- package/package.json +4 -1
package/dist/ui/App.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import { Box, Text, useApp, useStdin, useStdout, useWindowSize } from 'ink';
|
|
4
5
|
import { ProviderManager } from '../providers/provider-manager.js';
|
|
5
6
|
import { PlayerController } from '../player/player-controller.js';
|
|
6
7
|
import { playbackBackendInstallHint, playbackBackendLabel } from '../player/backend-install.js';
|
|
7
8
|
import { JsonLibraryStore, stationKey } from '../storage/store.js';
|
|
9
|
+
import { defaultAgentControlSettings } from '../types.js';
|
|
8
10
|
import { nextReceiverStyle, nextTheme, textDim, textMuted, themeAccent } from './theme.js';
|
|
9
11
|
import { DisplayContext, resolveDisplayMode } from './display-context.js';
|
|
10
|
-
import { homeItems,
|
|
12
|
+
import { homeItems, settingsItemsForPage } from './screen-items.js';
|
|
11
13
|
import { AppContent } from './AppContent.js';
|
|
12
14
|
import { TopTabs } from './components/TopTabs.js';
|
|
13
15
|
import { computeTerminalLayout } from './layout.js';
|
|
14
|
-
import {
|
|
16
|
+
import { truncate } from './format.js';
|
|
15
17
|
import { playbackFooterText, playbackStateForPendingStation } from './playback-footer.js';
|
|
16
18
|
import { balancedFooterLegendRows, fullFooterRowCount, fullStatusFooterRows, microPlaybackControlsText, microShortcutFooterText, pageFooterText } from './page-footer.js';
|
|
17
19
|
import { disableMouseReporting, enableMouseReporting, exploreCursorForMouseCell, shouldEnableMouseReporting } from './terminal-mouse.js';
|
|
@@ -22,15 +24,18 @@ import { isAirPlayBackendAvailable } from './airplay-settings.js';
|
|
|
22
24
|
import { audioOutputLabel, resolvedAudioOutput } from './audio-output.js';
|
|
23
25
|
import { copyToClipboard, openExternal } from './system-actions.js';
|
|
24
26
|
import { appVersion } from '../version.js';
|
|
25
|
-
import { checkForUpdate, installUpdate, shouldCheckForUpdate, updateCommandForInstall } from '../update-check.js';
|
|
27
|
+
import { automaticUpdateChecksAllowed, checkForUpdate, installUpdate, shouldCheckForUpdate, updateAvailableForVersion, updateCommandForInstall } from '../update-check.js';
|
|
26
28
|
import { helpItemCount } from './help-content.js';
|
|
27
29
|
import { EXIT_CONFIRMATION_MS, ctrlCExitDecision } from './exit-confirmation.js';
|
|
28
30
|
import { createAlarmTuiService, serializeAlarmTuiService } from './alarm-tui-service.js';
|
|
29
31
|
import { useAlarmTui } from './use-alarm-tui.js';
|
|
30
32
|
import { toAsciiSafe } from './ascii.js';
|
|
31
33
|
import { registerTuiPresence } from '../alarms/tui-presence.js';
|
|
34
|
+
import { configureMcpIntegrations } from '../agent/mcp-install.js';
|
|
35
|
+
import { startRadioSession } from '../agent/session.js';
|
|
32
36
|
import { activeTabForScreen, addMediaKeyBinding, applyStationFilters, clamp, clampVolume, defaultExploreCursor, formatExploreCursor, formatFilterLabel, formatTimeLeft, initialStationContexts, mediaActionLabel, moveExploreCursor as shiftExploreCursor, nextSleepTimerMinutes, normalizeMediaKeyBindings, shouldSkipAfterTuneError, stationApproximateTime, stationContextKeyForScreen, topTabs } from './app-state.js';
|
|
33
37
|
import { ReceiverAnimationProvider } from './receiver-animation.js';
|
|
38
|
+
import { VersionIndicator, versionIndicatorWidth } from './components/VersionIndicator.js';
|
|
34
39
|
const LOADING_SPINNER_MS = 120;
|
|
35
40
|
const VISUALIZER_MESSAGE_MS = 4500;
|
|
36
41
|
const LISTENING_HEARTBEAT_MS = 30_000;
|
|
@@ -44,9 +49,10 @@ const settingToggleLabel = {
|
|
|
44
49
|
transparentBackground: 'Transparent background',
|
|
45
50
|
asciiMode: 'ASCII-safe display',
|
|
46
51
|
reduceMotion: 'Reduce motion',
|
|
47
|
-
mouseSupport: 'Mouse and trackpad scrolling'
|
|
52
|
+
mouseSupport: 'Mouse and trackpad scrolling',
|
|
53
|
+
automaticUpdateChecks: 'Automatic update checks'
|
|
48
54
|
};
|
|
49
|
-
export function App({ store: providedStore, providers: providedProviders, alarmService: providedAlarmService, alarmPreview }) {
|
|
55
|
+
export function App({ store: providedStore, providers: providedProviders, alarmService: providedAlarmService, alarmPreview, initialAgentCommand, mcpConfigurator = configureMcpIntegrations, updateChecker = checkForUpdate }) {
|
|
50
56
|
const { exit } = useApp();
|
|
51
57
|
const { stdin } = useStdin();
|
|
52
58
|
const { stdout } = useStdout();
|
|
@@ -64,6 +70,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
64
70
|
const [availableAirPlayDevices, setAvailableAirPlayDevices] = useState(() => player.detectedAirPlayDevices());
|
|
65
71
|
const [screen, setScreen] = useState('home');
|
|
66
72
|
const [selected, setSelected] = useState(0);
|
|
73
|
+
const [settingsPage, setSettingsPage] = useState('root');
|
|
67
74
|
const [message, setMessage] = useState(null);
|
|
68
75
|
const [footerMessage, setFooterMessage] = useState(null);
|
|
69
76
|
const [countries, setCountries] = useState([]);
|
|
@@ -89,7 +96,9 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
89
96
|
const [showDiagnostics, setShowDiagnostics] = useState(false);
|
|
90
97
|
const [capturingTransportAction, setCapturingTransportAction] = useState(null);
|
|
91
98
|
const announcedUpdateRef = useRef(false);
|
|
99
|
+
const automaticUpdateCheckStartedRef = useRef(false);
|
|
92
100
|
const installingUpdateRef = useRef(false);
|
|
101
|
+
const configuringMcpRef = useRef(false);
|
|
93
102
|
const displayStationsRef = useRef([]);
|
|
94
103
|
const playbackQueueRef = useRef(null);
|
|
95
104
|
const lastRawTransportAtRef = useRef(0);
|
|
@@ -99,6 +108,8 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
99
108
|
const playStationRef = useRef(() => undefined);
|
|
100
109
|
const screenRef = useRef(screen);
|
|
101
110
|
const selectedRef = useRef(selected);
|
|
111
|
+
const settingsPageRef = useRef(settingsPage);
|
|
112
|
+
const selectedBySettingsPageRef = useRef({});
|
|
102
113
|
const selectedByScreenRef = useRef({});
|
|
103
114
|
const stationContextsRef = useRef(stationContexts);
|
|
104
115
|
const lastStationContextKeyRef = useRef('explore');
|
|
@@ -118,6 +129,9 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
118
129
|
const nearbyRequestRef = useRef(0);
|
|
119
130
|
const skipBrokenTimerRef = useRef(null);
|
|
120
131
|
const volumeRequestRef = useRef(0);
|
|
132
|
+
const agentHandlerRef = useRef(async () => {
|
|
133
|
+
throw new Error('RadioCLI is still starting.');
|
|
134
|
+
});
|
|
121
135
|
const theme = library.settings.theme;
|
|
122
136
|
const displayMode = useMemo(() => resolveDisplayMode(library.settings), [library.settings.transparentBackground, library.settings.asciiMode, library.settings.reduceMotion]);
|
|
123
137
|
const favoriteKeys = useMemo(() => new Set(library.favorites.map(stationKey)), [library.favorites]);
|
|
@@ -141,6 +155,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
141
155
|
}), [library, libraryStations, stationContexts]);
|
|
142
156
|
screenRef.current = screen;
|
|
143
157
|
selectedRef.current = selected;
|
|
158
|
+
settingsPageRef.current = settingsPage;
|
|
144
159
|
loadingStationsRef.current = loadingStations;
|
|
145
160
|
stationContextsRef.current = activeStationContexts;
|
|
146
161
|
exploreCursorRef.current = exploreCursor;
|
|
@@ -171,7 +186,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
171
186
|
'alarm-ringing': 0,
|
|
172
187
|
'airplay-settings': 0,
|
|
173
188
|
'airplay-code': 1,
|
|
174
|
-
settings:
|
|
189
|
+
settings: settingsItemsForPage(settingsPage).length,
|
|
175
190
|
help: helpItemCount
|
|
176
191
|
});
|
|
177
192
|
itemCountsRef.current = {
|
|
@@ -191,7 +206,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
191
206
|
'alarm-ringing': 0,
|
|
192
207
|
'airplay-settings': availableAirPlayDevices.length,
|
|
193
208
|
'airplay-code': 1,
|
|
194
|
-
settings:
|
|
209
|
+
settings: settingsItemsForPage(settingsPage).length,
|
|
195
210
|
help: helpItemCount
|
|
196
211
|
};
|
|
197
212
|
const displayStations = useMemo(() => applyStationFilters(stationContext.stations, filters), [filters, stationContext.stations]);
|
|
@@ -345,14 +360,16 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
345
360
|
refreshProviderHealth();
|
|
346
361
|
}, [refreshProviderHealth]);
|
|
347
362
|
const refreshUpdateCheck = useCallback(async () => {
|
|
348
|
-
const updateCheck = await
|
|
363
|
+
const updateCheck = await updateChecker({ currentVersion: installedVersion });
|
|
349
364
|
setLibrary(store.updateCheckState(updateCheck));
|
|
350
365
|
return updateCheck;
|
|
351
|
-
}, [installedVersion, store]);
|
|
366
|
+
}, [installedVersion, store, updateChecker]);
|
|
352
367
|
useEffect(() => {
|
|
353
|
-
if (
|
|
368
|
+
if (automaticUpdateCheckStartedRef.current ||
|
|
369
|
+
!automaticUpdateChecksAllowed(library.settings.automaticUpdateChecks !== false)) {
|
|
354
370
|
return;
|
|
355
371
|
}
|
|
372
|
+
automaticUpdateCheckStartedRef.current = true;
|
|
356
373
|
let cancelled = false;
|
|
357
374
|
void refreshUpdateCheck().then(updateCheck => {
|
|
358
375
|
if (cancelled) {
|
|
@@ -366,7 +383,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
366
383
|
return () => {
|
|
367
384
|
cancelled = true;
|
|
368
385
|
};
|
|
369
|
-
}, [library.
|
|
386
|
+
}, [library.settings.automaticUpdateChecks, refreshUpdateCheck]);
|
|
370
387
|
const setStationContextFor = useCallback((key, context) => {
|
|
371
388
|
setStationContexts(current => ({ ...current, [key]: context }));
|
|
372
389
|
}, []);
|
|
@@ -414,6 +431,15 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
414
431
|
const destination = screenRef.current === 'help' && next === 'home'
|
|
415
432
|
? helpReturnScreenRef.current
|
|
416
433
|
: next;
|
|
434
|
+
if (destination === 'settings' &&
|
|
435
|
+
screenRef.current !== 'settings' &&
|
|
436
|
+
screenRef.current !== 'airplay-settings' &&
|
|
437
|
+
screenRef.current !== 'airplay-code') {
|
|
438
|
+
settingsPageRef.current = 'root';
|
|
439
|
+
setSettingsPage('root');
|
|
440
|
+
itemCountsRef.current.settings = settingsItemsForPage('root').length;
|
|
441
|
+
selectedByScreenRef.current.settings = selectedBySettingsPageRef.current.root ?? 0;
|
|
442
|
+
}
|
|
417
443
|
selectedByScreenRef.current[screenRef.current] = selectedRef.current;
|
|
418
444
|
const remembered = selectedByScreenRef.current[destination] ?? 0;
|
|
419
445
|
const nextSelection = options.resetSelection ? 0 : remembered;
|
|
@@ -423,6 +449,22 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
423
449
|
setMessage(null);
|
|
424
450
|
}
|
|
425
451
|
}, []);
|
|
452
|
+
const openSettingsPage = useCallback((next) => {
|
|
453
|
+
selectedBySettingsPageRef.current[settingsPageRef.current] = selectedRef.current;
|
|
454
|
+
settingsPageRef.current = next;
|
|
455
|
+
setSettingsPage(next);
|
|
456
|
+
const nextSelection = clamp(selectedBySettingsPageRef.current[next] ?? 0, settingsItemsForPage(next).length - 1);
|
|
457
|
+
selectedRef.current = nextSelection;
|
|
458
|
+
setSelected(nextSelection);
|
|
459
|
+
setMessage(null);
|
|
460
|
+
}, []);
|
|
461
|
+
const closeSettingsPage = useCallback(() => {
|
|
462
|
+
if (settingsPageRef.current === 'root') {
|
|
463
|
+
go('home');
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
openSettingsPage('root');
|
|
467
|
+
}, [go, openSettingsPage]);
|
|
426
468
|
const openAirPlayCode = useCallback(() => {
|
|
427
469
|
setAirPlayCode('');
|
|
428
470
|
go('airplay-code', { resetSelection: true, clearMessage: false });
|
|
@@ -865,6 +907,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
865
907
|
await player.play(station, resolved.url);
|
|
866
908
|
if (requestId !== tuneRequestRef.current)
|
|
867
909
|
return;
|
|
910
|
+
playingStationRef.current = station;
|
|
868
911
|
setPlayingStation(station);
|
|
869
912
|
setTuningStation(null);
|
|
870
913
|
playbackQueueRef.current = queue;
|
|
@@ -1184,6 +1227,59 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1184
1227
|
updateSettings({ [key]: next });
|
|
1185
1228
|
setMessage(`${settingToggleLabel[key]} ${next ? 'on' : 'off'}.`);
|
|
1186
1229
|
}, [updateSettings]);
|
|
1230
|
+
const toggleAgentSetting = useCallback((key) => {
|
|
1231
|
+
const agentControl = settingsRef.current.agentControl ?? defaultAgentControlSettings;
|
|
1232
|
+
const next = !agentControl[key];
|
|
1233
|
+
updateSettings({ agentControl: { ...agentControl, [key]: next } });
|
|
1234
|
+
setMessage(`Agent ${key === 'openUiOnPlay' ? 'TUI launch' : 'Now Playing focus'} ${next ? 'on' : 'off'}.`);
|
|
1235
|
+
}, [updateSettings]);
|
|
1236
|
+
const setAgentIntegrationEnabled = useCallback(async () => {
|
|
1237
|
+
if (configuringMcpRef.current) {
|
|
1238
|
+
setMessage('Agent integration setup is already in progress.');
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
configuringMcpRef.current = true;
|
|
1242
|
+
const enabled = !(settingsRef.current.agentControl ?? defaultAgentControlSettings).enabled;
|
|
1243
|
+
setMessage(enabled ? 'Setting up agent and Codex Voice control...' : 'Disabling agent and voice control...');
|
|
1244
|
+
try {
|
|
1245
|
+
const results = await mcpConfigurator(enabled, mcpRuntime(), null);
|
|
1246
|
+
const failed = results.filter(item => item.status === 'failed');
|
|
1247
|
+
const agentControl = settingsRef.current.agentControl ?? defaultAgentControlSettings;
|
|
1248
|
+
updateSettings({ agentControl: { ...agentControl, enabled } });
|
|
1249
|
+
setMessage(integrationResultMessage(enabled, results, failed));
|
|
1250
|
+
}
|
|
1251
|
+
catch (error) {
|
|
1252
|
+
if (!enabled) {
|
|
1253
|
+
const agentControl = settingsRef.current.agentControl ?? defaultAgentControlSettings;
|
|
1254
|
+
updateSettings({ agentControl: { ...agentControl, enabled: false } });
|
|
1255
|
+
}
|
|
1256
|
+
setMessage(`Agent integration ${enabled ? 'setup' : 'removal'} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1257
|
+
}
|
|
1258
|
+
finally {
|
|
1259
|
+
configuringMcpRef.current = false;
|
|
1260
|
+
}
|
|
1261
|
+
}, [mcpConfigurator, updateSettings]);
|
|
1262
|
+
const repairMcpIntegrations = useCallback(async () => {
|
|
1263
|
+
if (configuringMcpRef.current) {
|
|
1264
|
+
setMessage('Agent integration setup is already in progress.');
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
configuringMcpRef.current = true;
|
|
1268
|
+
setMessage('Checking and repairing agent integrations...');
|
|
1269
|
+
try {
|
|
1270
|
+
const results = await mcpConfigurator(true, mcpRuntime(), null);
|
|
1271
|
+
const failed = results.filter(item => item.status === 'failed');
|
|
1272
|
+
const agentControl = settingsRef.current.agentControl ?? defaultAgentControlSettings;
|
|
1273
|
+
updateSettings({ agentControl: { ...agentControl, enabled: true } });
|
|
1274
|
+
setMessage(integrationResultMessage(true, results, failed));
|
|
1275
|
+
}
|
|
1276
|
+
catch (error) {
|
|
1277
|
+
setMessage(`MCP repair failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1278
|
+
}
|
|
1279
|
+
finally {
|
|
1280
|
+
configuringMcpRef.current = false;
|
|
1281
|
+
}
|
|
1282
|
+
}, [mcpConfigurator, updateSettings]);
|
|
1187
1283
|
const cycleSleepTimer = useCallback(() => {
|
|
1188
1284
|
const currentMinutes = sleepUntil ? Math.round((sleepUntil - Date.now()) / 60000) : null;
|
|
1189
1285
|
const next = nextSleepTimerMinutes(currentMinutes);
|
|
@@ -1273,7 +1369,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1273
1369
|
return;
|
|
1274
1370
|
}
|
|
1275
1371
|
const currentUpdateCheck = store.snapshot().updateCheck ?? library.updateCheck;
|
|
1276
|
-
if (!currentUpdateCheck
|
|
1372
|
+
if (!updateAvailableForVersion(currentUpdateCheck, installedVersion)) {
|
|
1277
1373
|
setMessage('Checking for updates...');
|
|
1278
1374
|
const latest = await refreshUpdateCheck();
|
|
1279
1375
|
if (latest.error) {
|
|
@@ -1293,12 +1389,25 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1293
1389
|
const result = await installUpdate(command.command);
|
|
1294
1390
|
installingUpdateRef.current = false;
|
|
1295
1391
|
if (result.ok) {
|
|
1296
|
-
|
|
1392
|
+
const agentControl = settingsRef.current.agentControl ?? defaultAgentControlSettings;
|
|
1393
|
+
if (agentControl.enabled) {
|
|
1394
|
+
try {
|
|
1395
|
+
const repaired = await mcpConfigurator(true, mcpRuntime(), null);
|
|
1396
|
+
const suffix = repaired.some(item => item.status === 'failed') ? ' Some MCP clients need manual repair.' : ' MCP integrations repaired.';
|
|
1397
|
+
setMessage(`Update installed.${suffix} Restart RadioCLI and open agent clients.`);
|
|
1398
|
+
}
|
|
1399
|
+
catch {
|
|
1400
|
+
setMessage('Update installed. MCP repair failed; run radiocli mcp repair, then restart RadioCLI and open agent clients.');
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
else {
|
|
1404
|
+
setMessage('Update installed. Restart RadioCLI to use the new version.');
|
|
1405
|
+
}
|
|
1297
1406
|
return;
|
|
1298
1407
|
}
|
|
1299
1408
|
const detail = result.output ? ` ${result.output.split('\n').at(-1)}` : '';
|
|
1300
1409
|
setMessage(`Update install failed. Run manually: ${result.command}.${detail}`);
|
|
1301
|
-
}, [installedVersion, library.updateCheck, refreshUpdateCheck, store]);
|
|
1410
|
+
}, [installedVersion, library.updateCheck, mcpConfigurator, refreshUpdateCheck, store]);
|
|
1302
1411
|
const executeCommand = useCommandExecutor({
|
|
1303
1412
|
beginLearningTransportKey,
|
|
1304
1413
|
countries,
|
|
@@ -1350,6 +1459,160 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1350
1459
|
void playStation(nextStation, { queue });
|
|
1351
1460
|
}
|
|
1352
1461
|
}, [playStation, playingStation, queueContainsStation, queueFromCurrentList, rememberQueueSelection]);
|
|
1462
|
+
agentHandlerRef.current = async (command) => {
|
|
1463
|
+
const sessionStatus = () => ({
|
|
1464
|
+
owner: 'tui',
|
|
1465
|
+
playback: player.getState(),
|
|
1466
|
+
station: playingStationRef.current,
|
|
1467
|
+
queue: playbackQueueRef.current?.stations ?? [],
|
|
1468
|
+
output: {
|
|
1469
|
+
preferredBackend: settingsRef.current.preferredBackend,
|
|
1470
|
+
preferredAirPlayDevice: settingsRef.current.preferredAirPlayDevice
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
const respond = (message, ok = true, data) => ({ ok, message, status: sessionStatus(), ...(data ? { data } : {}) });
|
|
1474
|
+
if (command.type === 'status')
|
|
1475
|
+
return respond(playingStationRef.current ? `${player.getState().state}: ${playingStationRef.current.name}` : 'RadioCLI is idle.');
|
|
1476
|
+
if (command.type === 'play') {
|
|
1477
|
+
if (command.ifPlaying === 'keep' && ['playing', 'paused', 'loading'].includes(player.getState().state)) {
|
|
1478
|
+
return respond(`Kept current station ${playingStationRef.current?.name ?? ''}.`);
|
|
1479
|
+
}
|
|
1480
|
+
const queue = { title: 'Agent selection', sourceScreen: 'now-playing', sourceContextKey: null, stations: command.queue?.length ? command.queue : [command.station] };
|
|
1481
|
+
await playStation(command.station, { queue, openNowPlaying: command.openNowPlaying });
|
|
1482
|
+
const state = player.getState();
|
|
1483
|
+
const playingRequestedStation = playingStationRef.current
|
|
1484
|
+
&& stationKey(playingStationRef.current) === stationKey(command.station)
|
|
1485
|
+
&& state.state === 'playing'
|
|
1486
|
+
&& state.ready;
|
|
1487
|
+
return playingRequestedStation
|
|
1488
|
+
? respond(`Playing ${command.station.name}.`)
|
|
1489
|
+
: respond(state.state === 'error' ? state.message ?? `Could not play ${command.station.name}.` : `Could not play ${command.station.name}.`, false);
|
|
1490
|
+
}
|
|
1491
|
+
if (command.type === 'pause') {
|
|
1492
|
+
const control = await player.pause();
|
|
1493
|
+
return respond(control.message ?? 'Paused.', control.ok);
|
|
1494
|
+
}
|
|
1495
|
+
if (command.type === 'resume') {
|
|
1496
|
+
const control = await player.resume();
|
|
1497
|
+
return respond(control.message ?? 'Resumed.', control.ok);
|
|
1498
|
+
}
|
|
1499
|
+
if (command.type === 'stop') {
|
|
1500
|
+
tuneRequestRef.current += 1;
|
|
1501
|
+
setLibrary(store.finishActiveListeningSession());
|
|
1502
|
+
await player.stop();
|
|
1503
|
+
playingStationRef.current = null;
|
|
1504
|
+
setPlayingStation(null);
|
|
1505
|
+
setTuningStation(null);
|
|
1506
|
+
playbackQueueRef.current = null;
|
|
1507
|
+
return respond('Stopped RadioCLI.');
|
|
1508
|
+
}
|
|
1509
|
+
if (command.type === 'alarm-preempt') {
|
|
1510
|
+
tuneRequestRef.current += 1;
|
|
1511
|
+
setLibrary(store.finishActiveListeningSession());
|
|
1512
|
+
await player.stop();
|
|
1513
|
+
playingStationRef.current = null;
|
|
1514
|
+
setPlayingStation(null);
|
|
1515
|
+
setTuningStation(null);
|
|
1516
|
+
playbackQueueRef.current = null;
|
|
1517
|
+
return respond('Interactive playback yielded to the alarm.');
|
|
1518
|
+
}
|
|
1519
|
+
if (command.type === 'set-volume') {
|
|
1520
|
+
const control = await player.setVolume(clampVolume(command.volume));
|
|
1521
|
+
if (control.ok)
|
|
1522
|
+
updateSettings({ volume: player.getState().volume });
|
|
1523
|
+
return respond(control.message ?? `Volume ${player.getState().volume}.`, control.ok);
|
|
1524
|
+
}
|
|
1525
|
+
if (command.type === 'set-muted') {
|
|
1526
|
+
const control = await player.setMuted(command.muted);
|
|
1527
|
+
return respond(control.message ?? (command.muted ? 'Muted.' : 'Unmuted.'), control.ok);
|
|
1528
|
+
}
|
|
1529
|
+
if (command.type === 'set-favorite') {
|
|
1530
|
+
const station = command.station ?? playingStationRef.current;
|
|
1531
|
+
if (!station)
|
|
1532
|
+
return respond('No active station to favorite.', false);
|
|
1533
|
+
const current = store.isFavorite(station);
|
|
1534
|
+
if (current !== command.favorite)
|
|
1535
|
+
toggleFavorite(station);
|
|
1536
|
+
return respond(`${command.favorite ? 'Favorited' : 'Removed favorite'}: ${station.name}.`);
|
|
1537
|
+
}
|
|
1538
|
+
if (command.type === 'airplay-list') {
|
|
1539
|
+
const devices = await refreshAirPlayTargets(false);
|
|
1540
|
+
return respond(devices.length ? `${devices.length} AirPlay receiver(s) found.` : 'No AirPlay receivers found.', true, devices);
|
|
1541
|
+
}
|
|
1542
|
+
if (command.type === 'airplay-select') {
|
|
1543
|
+
const devices = await refreshAirPlayTargets(false);
|
|
1544
|
+
const device = devices.find(item => item.id === command.deviceId);
|
|
1545
|
+
if (!device)
|
|
1546
|
+
return respond('AirPlay receiver not found. Refresh and use an exact receiver ID.', false, devices);
|
|
1547
|
+
if (device.local) {
|
|
1548
|
+
const backend = preferredLocalPlaybackBackend(availableBackends);
|
|
1549
|
+
if (!backend)
|
|
1550
|
+
return respond('No local playback backend is available. Run radiocli setup to install mpv.', false);
|
|
1551
|
+
updateSettings({ preferredBackend: backend });
|
|
1552
|
+
if (playingStationRef.current && shouldRetuneForAudioOutput(player.getState().state)) {
|
|
1553
|
+
await playStation(playingStationRef.current, { queue: playbackQueueRef.current ?? queueFromCurrentList(playingStationRef.current) });
|
|
1554
|
+
}
|
|
1555
|
+
return respond(`Audio output set to this device (${backend}).`);
|
|
1556
|
+
}
|
|
1557
|
+
if (!isAirPlayBackendAvailable(availableBackends))
|
|
1558
|
+
return respond('AirPlay playback is unavailable; run radiocli doctor.', false);
|
|
1559
|
+
updateSettings({ preferredAirPlayDevice: device.id });
|
|
1560
|
+
updateSettings({ preferredBackend: 'airplay' });
|
|
1561
|
+
if (playingStationRef.current && shouldRetuneForAudioOutput(player.getState().state)) {
|
|
1562
|
+
await playStation(playingStationRef.current, { queue: playbackQueueRef.current ?? queueFromCurrentList(playingStationRef.current) });
|
|
1563
|
+
}
|
|
1564
|
+
return respond(`Audio output set to AirPlay receiver ${device.name}.`);
|
|
1565
|
+
}
|
|
1566
|
+
if (command.type === 'airplay-local') {
|
|
1567
|
+
const backend = preferredLocalPlaybackBackend(availableBackends);
|
|
1568
|
+
if (!backend)
|
|
1569
|
+
return respond('No local playback backend is available. Run radiocli setup to install mpv.', false);
|
|
1570
|
+
updateSettings({ preferredBackend: backend });
|
|
1571
|
+
if (playingStationRef.current && shouldRetuneForAudioOutput(player.getState().state)) {
|
|
1572
|
+
await playStation(playingStationRef.current, { queue: playbackQueueRef.current ?? queueFromCurrentList(playingStationRef.current) });
|
|
1573
|
+
}
|
|
1574
|
+
return respond(`Audio output set to this device (${backend}).`);
|
|
1575
|
+
}
|
|
1576
|
+
if (command.type === 'airplay-passcode') {
|
|
1577
|
+
const control = player.submitAirPlayPasscode(command.code);
|
|
1578
|
+
return respond(control.message ?? 'AirPlay code sent.', control.ok);
|
|
1579
|
+
}
|
|
1580
|
+
if (command.type === 'update-settings') {
|
|
1581
|
+
updateSettings(command.settings);
|
|
1582
|
+
return respond('RadioCLI settings updated.');
|
|
1583
|
+
}
|
|
1584
|
+
const active = playingStationRef.current;
|
|
1585
|
+
const queue = playbackQueueRef.current;
|
|
1586
|
+
if (!active || !queue?.stations.length)
|
|
1587
|
+
return respond('No playback queue is available.', false);
|
|
1588
|
+
const currentIndex = queue.stations.findIndex(item => stationKey(item) === stationKey(active));
|
|
1589
|
+
const delta = command.type === 'next' ? 1 : -1;
|
|
1590
|
+
const next = queue.stations[(Math.max(0, currentIndex) + delta + queue.stations.length) % queue.stations.length];
|
|
1591
|
+
if (!next)
|
|
1592
|
+
return respond('No adjacent station is available.', false);
|
|
1593
|
+
await playStation(next, { queue });
|
|
1594
|
+
return respond(`Playing ${next.name}.`);
|
|
1595
|
+
};
|
|
1596
|
+
useEffect(() => {
|
|
1597
|
+
if (!stdin.isTTY || !(settingsRef.current.agentControl ?? defaultAgentControlSettings).enabled)
|
|
1598
|
+
return;
|
|
1599
|
+
let disposed = false;
|
|
1600
|
+
let close;
|
|
1601
|
+
void startRadioSession(command => agentHandlerRef.current(command)).then(async (session) => {
|
|
1602
|
+
if (disposed) {
|
|
1603
|
+
await session.close();
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
close = session.close;
|
|
1607
|
+
if (initialAgentCommand)
|
|
1608
|
+
await agentHandlerRef.current(initialAgentCommand);
|
|
1609
|
+
}).catch(error => setMessage(error instanceof Error ? error.message : 'Agent controls could not start.'));
|
|
1610
|
+
return () => {
|
|
1611
|
+
disposed = true;
|
|
1612
|
+
if (close)
|
|
1613
|
+
void close();
|
|
1614
|
+
};
|
|
1615
|
+
}, [initialAgentCommand, library.settings.agentControl?.enabled, stdin]);
|
|
1353
1616
|
useAppInput({
|
|
1354
1617
|
adjustVolume,
|
|
1355
1618
|
airPlayCode,
|
|
@@ -1378,6 +1641,8 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1378
1641
|
openAdjacentTab,
|
|
1379
1642
|
openAirPlayCode,
|
|
1380
1643
|
openAirPlaySettings,
|
|
1644
|
+
openSettingsPage,
|
|
1645
|
+
closeSettingsPage,
|
|
1381
1646
|
openScreen,
|
|
1382
1647
|
handleAlarmInput: alarmTui.handleInput,
|
|
1383
1648
|
openAlarmForStation: alarmTui.openForStation,
|
|
@@ -1394,9 +1659,12 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1394
1659
|
void refreshAirPlayTargets();
|
|
1395
1660
|
},
|
|
1396
1661
|
resetLearnedTransportKeys,
|
|
1662
|
+
repairMcpIntegrations,
|
|
1663
|
+
setAgentIntegrationEnabled,
|
|
1397
1664
|
runSearch,
|
|
1398
1665
|
saveLearnedTransportKey,
|
|
1399
1666
|
screen,
|
|
1667
|
+
settingsPage,
|
|
1400
1668
|
searchQuery,
|
|
1401
1669
|
selectedRef,
|
|
1402
1670
|
selectedStationForInput,
|
|
@@ -1420,6 +1688,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1420
1688
|
toggleMute,
|
|
1421
1689
|
togglePause,
|
|
1422
1690
|
toggleSetting,
|
|
1691
|
+
toggleAgentSetting,
|
|
1423
1692
|
toggleNearbyLocation,
|
|
1424
1693
|
toggleDirectoryVoting,
|
|
1425
1694
|
toggleRadioGarden,
|
|
@@ -1449,7 +1718,8 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1449
1718
|
editingCountryFilter,
|
|
1450
1719
|
editingSearch,
|
|
1451
1720
|
playbackBackend: playback.backend,
|
|
1452
|
-
screen
|
|
1721
|
+
screen,
|
|
1722
|
+
settingsPage
|
|
1453
1723
|
});
|
|
1454
1724
|
const pageFooter = alarmTui.activeAlarms.length > 0 && screen !== 'alarm-ringing'
|
|
1455
1725
|
? `ALARM PLAYING · ! controls · ${basePageFooter}`
|
|
@@ -1468,8 +1738,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1468
1738
|
? microPlaybackControlsText(playback.backend)
|
|
1469
1739
|
: pageFooter);
|
|
1470
1740
|
const compactGlobalFooter = '←/→ tabs · ? help · q quit';
|
|
1471
|
-
const
|
|
1472
|
-
const versionReserve = displayWidth(versionLabel) + 2;
|
|
1741
|
+
const versionReserve = versionIndicatorWidth(installedVersion, library.updateCheck) + 2;
|
|
1473
1742
|
const fullStatusRows = fullStatusFooterRows(screen, message, footerMessage, playbackFooter);
|
|
1474
1743
|
const fullLegendRows = balancedFooterLegendRows(pageFooter, globalFooter, frameWidth, 2, versionReserve);
|
|
1475
1744
|
const compactStatus = footerMessage ?? message ?? playbackFooter;
|
|
@@ -1480,7 +1749,19 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1480
1749
|
? microFooter
|
|
1481
1750
|
: microShortcutFooterText(microFooter, microLegendWidth);
|
|
1482
1751
|
const footerText = (value) => displayMode.ascii ? toAsciiSafe(value) : value;
|
|
1483
|
-
return (_jsx(ReceiverAnimationProvider, { screen: screen, playback: playback, receiverStyle: library.settings.receiverStyle, reduceMotion: Boolean(library.settings.reduceMotion), children: _jsx(DisplayContext.Provider, { value: displayMode, children: _jsxs(Box, { flexDirection: "column", paddingX: layout.horizontalPadding, height: layout.rows, width: layout.columns, overflow: "hidden", backgroundColor: displayMode.app, children: [hasTopTabs ? (_jsx(Box, { height: 3, flexShrink: 0, backgroundColor: displayMode.app, children: _jsx(TopTabs, { tabs: topTabs, active: activeTabForScreen(screen), theme: theme, width: frameWidth, backendLabel: playbackBackendLabel(playback.backend) }) })) : null, _jsx(Box, { height: layout.contentRows, width: frameWidth, flexDirection: "column", overflowY: "hidden", flexShrink: 0, backgroundColor: displayMode.app, children: _jsx(AppContent, { airPlayDevices: availableAirPlayDevices, airPlayCode: airPlayCode, appVersion: installedVersion, backends: availableBackends, countryFilter: countryFilter, diagnostics: diagnostics, displayStations: displayStations, editingCountryFilter: editingCountryFilter, editingSearch: editingSearch, favoriteKeys: favoriteKeys, filterLabel: filterLabel, filteredCountries: filteredCountries, frameWidth: frameWidth, layout: layout, library: library, loadingCountries: loadingCountries, loadingStations: loadingStations, nowPlaying: nowPlaying, playback: footerPlayback, playingStation: footerStation, providerHealth: providerHealth, searchQuery: searchQuery, screen: screen, selected: selected, showDiagnostics: showDiagnostics, sleepLabel: sleepLabel, stationContext: stationContext, exploreCursor: exploreCursor, stationFavorite: store.isFavorite(footerStation), stationTime: stationApproximateTime(footerStation), storePath: store.filePath, theme: theme, updateCheck: library.updateCheck, alarmTui: alarmTui }) }), _jsx(Box, { height: layout.footerRows, width: frameWidth, flexDirection: "column", flexShrink: 0, backgroundColor: displayMode.app, children: layout.mode === 'full' ? (_jsxs(_Fragment, { children: [fullStatusRows.map(statusRow => (_jsx(Text, { color: themeAccent(theme), children: footerText(truncate(statusRow.text, frameWidth)) }, statusRow.key))), _jsx(Text, { color: commandMode || capturingTransportAction ? themeAccent(theme) : textMuted, children: footerText(fullLegendRows[0] ?? ' ') }), _jsxs(Box, { children: [_jsx(Text, { color: textDim, children: footerText(fullLegendRows[1] ?? ' ') }), _jsx(Box, { flexGrow: 1 }), _jsx(
|
|
1752
|
+
return (_jsx(ReceiverAnimationProvider, { screen: screen, playback: playback, receiverStyle: library.settings.receiverStyle, reduceMotion: Boolean(library.settings.reduceMotion), children: _jsx(DisplayContext.Provider, { value: displayMode, children: _jsxs(Box, { flexDirection: "column", paddingX: layout.horizontalPadding, height: layout.rows, width: layout.columns, overflow: "hidden", backgroundColor: displayMode.app, children: [hasTopTabs ? (_jsx(Box, { height: 3, flexShrink: 0, backgroundColor: displayMode.app, children: _jsx(TopTabs, { tabs: topTabs, active: activeTabForScreen(screen), theme: theme, width: frameWidth, backendLabel: playbackBackendLabel(playback.backend) }) })) : null, _jsx(Box, { height: layout.contentRows, width: frameWidth, flexDirection: "column", overflowY: "hidden", flexShrink: 0, backgroundColor: displayMode.app, children: _jsx(AppContent, { airPlayDevices: availableAirPlayDevices, airPlayCode: airPlayCode, appVersion: installedVersion, backends: availableBackends, countryFilter: countryFilter, diagnostics: diagnostics, displayStations: displayStations, editingCountryFilter: editingCountryFilter, editingSearch: editingSearch, favoriteKeys: favoriteKeys, filterLabel: filterLabel, filteredCountries: filteredCountries, frameWidth: frameWidth, layout: layout, library: library, loadingCountries: loadingCountries, loadingStations: loadingStations, nowPlaying: nowPlaying, playback: footerPlayback, playingStation: footerStation, providerHealth: providerHealth, searchQuery: searchQuery, screen: screen, settingsPage: settingsPage, selected: selected, showDiagnostics: showDiagnostics, sleepLabel: sleepLabel, stationContext: stationContext, exploreCursor: exploreCursor, stationFavorite: store.isFavorite(footerStation), stationTime: stationApproximateTime(footerStation), storePath: store.filePath, theme: theme, updateCheck: library.updateCheck, alarmTui: alarmTui }) }), _jsx(Box, { height: layout.footerRows, width: frameWidth, flexDirection: "column", flexShrink: 0, backgroundColor: displayMode.app, children: layout.mode === 'full' ? (_jsxs(_Fragment, { children: [fullStatusRows.map(statusRow => (_jsx(Text, { color: themeAccent(theme), children: footerText(truncate(statusRow.text, frameWidth)) }, statusRow.key))), _jsx(Text, { color: commandMode || capturingTransportAction ? themeAccent(theme) : textMuted, children: footerText(fullLegendRows[0] ?? ' ') }), _jsxs(Box, { children: [_jsx(Text, { color: textDim, children: footerText(fullLegendRows[1] ?? ' ') }), _jsx(Box, { flexGrow: 1 }), _jsx(VersionIndicator, { currentVersion: installedVersion, updateCheck: library.updateCheck, theme: theme })] })] })) : (_jsx(_Fragment, { children: layout.mode === 'micro' ? _jsxs(Box, { children: [_jsx(Text, { color: commandMode || capturingTransportAction || footerMessage || message ? themeAccent(theme) : textMuted, children: footerText(truncate(microLegend, microLegendWidth)) }), _jsx(Box, { flexGrow: 1 }), _jsx(VersionIndicator, { currentVersion: installedVersion, updateCheck: library.updateCheck, theme: theme })] }) : _jsxs(_Fragment, { children: [compactStatus ? _jsx(Text, { color: footerMessage || message ? themeAccent(theme) : textMuted, children: footerText(truncate(compactStatus, frameWidth)) }) : null, compactLegendRows.map((row, index) => index === compactLegendRows.length - 1 ? _jsxs(Box, { children: [_jsx(Text, { color: textDim, children: footerText(row) }), _jsx(Box, { flexGrow: 1 }), _jsx(VersionIndicator, { currentVersion: installedVersion, updateCheck: library.updateCheck, theme: theme })] }, `legend-${index}`) : _jsx(Text, { color: index === 0 ? textMuted : textDim, children: footerText(row) }, `legend-${index}`))] }) })) })] }) }) }));
|
|
1753
|
+
}
|
|
1754
|
+
function mcpRuntime() {
|
|
1755
|
+
return { nodePath: process.execPath, cliPath: fileURLToPath(new URL('../cli.js', import.meta.url)) };
|
|
1756
|
+
}
|
|
1757
|
+
function integrationResultMessage(enabled, results, failed) {
|
|
1758
|
+
if (failed.length > 0) {
|
|
1759
|
+
return `${enabled ? 'Agent control enabled' : 'Agent control disabled'}, but ${failed.map(item => item.client).join(', ')} need attention. Run radiocli mcp status for details.`;
|
|
1760
|
+
}
|
|
1761
|
+
const configured = results.filter(item => item.status === (enabled ? 'configured' : 'removed')).map(item => item.client);
|
|
1762
|
+
if (!enabled)
|
|
1763
|
+
return 'Agent and voice control disabled; detected MCP registrations were removed.';
|
|
1764
|
+
return `Agent and voice control ready${configured.length ? ` for ${configured.join(', ')}` : ''}. Restart open Codex or agent clients, then ask them to control RadioCLI.`;
|
|
1484
1765
|
}
|
|
1485
1766
|
function buildLibraryStations(library) {
|
|
1486
1767
|
const stations = [];
|
package/dist/ui/AppContent.js
CHANGED
|
@@ -15,7 +15,7 @@ import { AirPlayCodeScreen } from './screens/AirPlayCodeScreen.js';
|
|
|
15
15
|
import { selectedAirPlayDevice } from './airplay-settings.js';
|
|
16
16
|
import { AdaptiveContent } from './AdaptiveContent.js';
|
|
17
17
|
import { AlarmsScreen, AlarmEditorScreen, AlarmPickerScreen, AlarmRingingScreen } from './screens/AlarmsScreen.js';
|
|
18
|
-
export function AppContent({ airPlayDevices, airPlayCode, appVersion, backends, countryFilter, diagnostics, displayStations, editingCountryFilter, editingSearch, exploreCursor, favoriteKeys, filterLabel, filteredCountries, frameWidth, layout, library, loadingCountries, loadingStations, nowPlaying, playback, playingStation, providerHealth, searchQuery, screen, selected, showDiagnostics, sleepLabel, stationContext, stationFavorite, stationTime, storePath, theme, updateCheck, alarmTui }) {
|
|
18
|
+
export function AppContent({ airPlayDevices, airPlayCode, appVersion, backends, countryFilter, diagnostics, displayStations, editingCountryFilter, editingSearch, exploreCursor, favoriteKeys, filterLabel, filteredCountries, frameWidth, layout, library, loadingCountries, loadingStations, nowPlaying, playback, playingStation, providerHealth, searchQuery, screen, settingsPage, selected, showDiagnostics, sleepLabel, stationContext, stationFavorite, stationTime, storePath, theme, updateCheck, alarmTui }) {
|
|
19
19
|
if (layout.compact) {
|
|
20
20
|
if (screen === 'alarms')
|
|
21
21
|
return _jsx(AlarmsScreen, { alarms: library.alarms, selected: selected, runtime: alarmTui.runtime, verification: alarmTui.verification, deletingId: alarmTui.deletingId, busyAlarmIds: alarmTui.busyAlarmIds, theme: theme, width: frameWidth, height: layout.contentRows, mode: layout.mode });
|
|
@@ -25,7 +25,7 @@ export function AppContent({ airPlayDevices, airPlayCode, appVersion, backends,
|
|
|
25
25
|
return _jsx(AlarmPickerScreen, { choices: alarmTui.pickerChoices, selected: selected, fallback: alarmTui.pickerFallback, theme: theme, width: frameWidth, height: layout.contentRows });
|
|
26
26
|
if (screen === 'alarm-ringing')
|
|
27
27
|
return _jsx(AlarmRingingScreen, { sessions: alarmTui.activeAlarms, alarms: library.alarms, selected: alarmTui.activeSelected, snoozeMinutes: alarmTui.snoozeMinutes, theme: theme, width: frameWidth, height: layout.contentRows });
|
|
28
|
-
return (_jsx(AdaptiveContent, { mode: layout.mode === 'micro' ? 'micro' : 'compact', screen: screen, selected: selected, height: layout.contentRows, width: frameWidth, theme: theme, playback: playback, playingStation: playingStation, nowPlaying: nowPlaying, stations: displayStations, countries: filteredCountries, airPlayDevices: airPlayDevices, airPlayCode: airPlayCode, searchQuery: searchQuery, editingSearch: editingSearch, countryFilter: countryFilter, editingCountryFilter: editingCountryFilter, loadingCountries: loadingCountries, loadingStations: loadingStations, exploreCursor: exploreCursor, library: library, diagnostics: diagnostics, backends: backends, updateCheck: updateCheck, favoriteKeys: favoriteKeys, stationTitle: stationContext.title, stationError: stationContext.error, filterLabel: filterLabel, sleepLabel: sleepLabel }));
|
|
28
|
+
return (_jsx(AdaptiveContent, { mode: layout.mode === 'micro' ? 'micro' : 'compact', screen: screen, settingsPage: settingsPage, selected: selected, height: layout.contentRows, width: frameWidth, theme: theme, playback: playback, playingStation: playingStation, nowPlaying: nowPlaying, stations: displayStations, countries: filteredCountries, airPlayDevices: airPlayDevices, airPlayCode: airPlayCode, searchQuery: searchQuery, editingSearch: editingSearch, countryFilter: countryFilter, editingCountryFilter: editingCountryFilter, loadingCountries: loadingCountries, loadingStations: loadingStations, exploreCursor: exploreCursor, library: library, diagnostics: diagnostics, backends: backends, updateCheck: updateCheck, appVersion: appVersion, favoriteKeys: favoriteKeys, stationTitle: stationContext.title, stationError: stationContext.error, filterLabel: filterLabel, sleepLabel: sleepLabel }));
|
|
29
29
|
}
|
|
30
30
|
if (screen === 'alarms')
|
|
31
31
|
return _jsx(AlarmsScreen, { alarms: library.alarms, selected: selected, runtime: alarmTui.runtime, verification: alarmTui.verification, deletingId: alarmTui.deletingId, busyAlarmIds: alarmTui.busyAlarmIds, theme: theme, width: frameWidth, height: layout.contentRows, mode: layout.mode });
|
|
@@ -39,7 +39,7 @@ export function AppContent({ airPlayDevices, airPlayCode, appVersion, backends,
|
|
|
39
39
|
return _jsx(HomeScreen, { selected: selected, theme: theme, library: library });
|
|
40
40
|
}
|
|
41
41
|
if (screen === 'countries') {
|
|
42
|
-
return (_jsx(CountriesScreen, { countries: filteredCountries, selected: selected, loading: loadingCountries, filter: countryFilter, editingFilter: editingCountryFilter, theme: theme, pageSize: layout.countryRows, width: frameWidth }));
|
|
42
|
+
return (_jsx(CountriesScreen, { countries: filteredCountries, selected: selected, loading: loadingCountries, filter: countryFilter, editingFilter: editingCountryFilter, theme: theme, pageSize: layout.countryRows, width: frameWidth, reduceMotion: Boolean(library.settings.reduceMotion) }));
|
|
43
43
|
}
|
|
44
44
|
if (screen === 'map') {
|
|
45
45
|
return (_jsx(MapScreen, { countries: filteredCountries, selected: selected, loading: loadingCountries, filter: countryFilter, editingFilter: editingCountryFilter, theme: theme, pageSize: layout.mapCountryRows, mode: layout.mapMode, width: frameWidth }));
|
|
@@ -60,7 +60,7 @@ export function AppContent({ airPlayDevices, airPlayCode, appVersion, backends,
|
|
|
60
60
|
return _jsx(StatsScreen, { library: library, theme: theme, width: frameWidth, height: layout.contentRows });
|
|
61
61
|
}
|
|
62
62
|
if (screen === 'settings') {
|
|
63
|
-
return (_jsx(SettingsScreen, { selected: selected, settings: library.settings, appVersion: appVersion, updateCheck: updateCheck, storePath: storePath, playback: playback, backends: backends, airPlayDevices: airPlayDevices, providerHealth: providerHealth, theme: theme, diagnostics: diagnostics, width: frameWidth, height: layout.contentRows }));
|
|
63
|
+
return (_jsx(SettingsScreen, { page: settingsPage, selected: selected, settings: library.settings, appVersion: appVersion, updateCheck: updateCheck, storePath: storePath, playback: playback, backends: backends, airPlayDevices: airPlayDevices, providerHealth: providerHealth, theme: theme, diagnostics: diagnostics, width: frameWidth, height: layout.contentRows }));
|
|
64
64
|
}
|
|
65
65
|
if (screen === 'airplay-settings') {
|
|
66
66
|
return (_jsx(AirPlaySettingsScreen, { selected: selected, settings: library.settings, backends: backends, devices: airPlayDevices, theme: theme, width: frameWidth, height: layout.contentRows }));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import {
|
|
3
|
+
import { stationLocation, stationTags, stationTech, truncate } from '../format.js';
|
|
4
4
|
import { textMuted, themeAccent } from '../theme.js';
|
|
5
5
|
import { Menu, Pointer } from './Menu.js';
|
|
6
6
|
import { visibleWindow } from '../list-window.js';
|
|
@@ -21,12 +21,10 @@ export function StationList({ stations, selected, theme, favorites, pageSize, wi
|
|
|
21
21
|
const compactNameWidth = Math.max(1, width - 2 - (favorite ? 2 : 0));
|
|
22
22
|
return (_jsxs(Box, { children: [_jsx(Pointer, { active: active }), _jsx(Text, { color: active ? themeAccent(theme) : undefined, bold: active, children: _jsx(AdaptiveMarquee, { text: station.name, width: compactNameWidth, active: active, reduceMotion: reduceMotion }) }), favorite ? _jsx(Text, { color: "yellow", children: " \u2605" }) : null] }));
|
|
23
23
|
}
|
|
24
|
-
const
|
|
24
|
+
const stationNameWidth = favorite ? Math.max(1, nameWidth - 2) : nameWidth;
|
|
25
25
|
const titleWidth = nameWidth + 2;
|
|
26
|
-
const titleUsed = displayWidth(stationName) + (favorite ? 2 : 0);
|
|
27
|
-
const titlePadding = ' '.repeat(Math.max(1, titleWidth - titleUsed));
|
|
28
26
|
const standardMetadata = `${stationLocation(station)} · ${stationTech(station)}`;
|
|
29
27
|
const selectedMetadata = station.tags.length > 0 ? stationTags(station) : standardMetadata;
|
|
30
|
-
return (_jsxs(Box, { children: [_jsx(Pointer, { active: active }), _jsx(Text, { color: active ? themeAccent(theme) : undefined, bold: active, children:
|
|
28
|
+
return (_jsxs(Box, { children: [_jsx(Pointer, { active: active }), _jsxs(Box, { width: titleWidth, children: [_jsx(Box, { width: stationNameWidth, children: _jsx(Text, { color: active ? themeAccent(theme) : undefined, bold: active, children: _jsx(AdaptiveMarquee, { text: station.name, width: stationNameWidth, active: active, reduceMotion: reduceMotion }) }) }), favorite ? _jsx(Text, { color: "yellow", children: " \u2605" }) : null] }), _jsx(Text, { color: active ? themeAccent(theme) : textMuted, children: truncate(active ? selectedMetadata : standardMetadata, metaWidth) })] }));
|
|
31
29
|
} })] }));
|
|
32
30
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { updateAvailableForVersion } from '../../update-check.js';
|
|
4
|
+
import { displayWidth } from '../format.js';
|
|
5
|
+
import { appBackground, textDim, themeAccent } from '../theme.js';
|
|
6
|
+
export function availableUpdateLabel(currentVersion, updateCheck) {
|
|
7
|
+
return updateAvailableForVersion(updateCheck, currentVersion) && updateCheck?.latestVersion
|
|
8
|
+
? `v${updateCheck.latestVersion} available`
|
|
9
|
+
: undefined;
|
|
10
|
+
}
|
|
11
|
+
export function versionIndicatorWidth(currentVersion, updateCheck) {
|
|
12
|
+
const versionWidth = displayWidth(`v${currentVersion}`);
|
|
13
|
+
const updateLabel = availableUpdateLabel(currentVersion, updateCheck);
|
|
14
|
+
return updateLabel ? displayWidth(updateLabel) + 3 + versionWidth : versionWidth;
|
|
15
|
+
}
|
|
16
|
+
export function VersionIndicator({ currentVersion, updateCheck, theme }) {
|
|
17
|
+
const updateLabel = availableUpdateLabel(currentVersion, updateCheck);
|
|
18
|
+
return (_jsxs(Box, { flexShrink: 0, children: [updateLabel ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: appBackground, backgroundColor: themeAccent(theme), bold: true, children: ` ${updateLabel} ` }), _jsx(Text, { children: " " })] })) : null, _jsx(Text, { color: textDim, children: `v${currentVersion}` })] }));
|
|
19
|
+
}
|
package/dist/ui/page-footer.js
CHANGED
|
@@ -13,7 +13,7 @@ export function fullStatusFooterRows(screen, message, footerMessage, playbackSta
|
|
|
13
13
|
{ key: 'playback', text: footerMessage ?? playbackStatus ?? ' ' }
|
|
14
14
|
];
|
|
15
15
|
}
|
|
16
|
-
export function pageFooterText({ capturingTransportAction, commandMode, commandText, editingCountryFilter, editingSearch, canEnterAirPlayCode, playbackBackend, screen }) {
|
|
16
|
+
export function pageFooterText({ capturingTransportAction, commandMode, commandText, editingCountryFilter, editingSearch, canEnterAirPlayCode, playbackBackend, screen, settingsPage = 'root' }) {
|
|
17
17
|
if (capturingTransportAction) {
|
|
18
18
|
return `Learn ${mediaActionLabel(capturingTransportAction)} key: press key · Esc cancel`;
|
|
19
19
|
}
|
|
@@ -58,7 +58,9 @@ export function pageFooterText({ capturingTransportAction, commandMode, commandT
|
|
|
58
58
|
return 'space/F8 pause · f favorite · m mute · s sleep · d diagnostics · b Overview';
|
|
59
59
|
}
|
|
60
60
|
if (screen === 'settings') {
|
|
61
|
-
return
|
|
61
|
+
return settingsPage === 'root'
|
|
62
|
+
? '↑/↓ choose · Enter open · o output · a AirPlay · b Overview'
|
|
63
|
+
: '↑/↓ choose · Enter change · b Settings · shortcuts still work';
|
|
62
64
|
}
|
|
63
65
|
if (screen === 'airplay-settings') {
|
|
64
66
|
return canEnterAirPlayCode
|