@ciphore/radiocli 0.2.3 → 0.2.4
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 +77 -0
- package/CONTRIBUTING.md +36 -6
- package/README.md +54 -10
- package/dist/agent/headless-host.js +39 -17
- package/dist/agent/launcher.js +6 -67
- package/dist/agent/mcp-install.js +15 -15
- package/dist/agent/service.js +3 -3
- package/dist/agent/session.js +13 -29
- package/dist/alarms/active-session.js +9 -12
- package/dist/alarms/guard.js +79 -36
- package/dist/alarms/inhibitor.js +27 -22
- package/dist/alarms/power-guard-store.js +2 -10
- package/dist/alarms/runner.js +56 -25
- package/dist/alarms/schedule.js +9 -2
- package/dist/alarms/scheduler.js +194 -52
- package/dist/alarms/setup-verification.js +1 -2
- package/dist/alarms/system-volume-ownership.js +267 -0
- package/dist/alarms/system-volume.js +155 -14
- package/dist/alarms/terminal-launcher.js +76 -136
- package/dist/alarms/tui-presence.js +2 -4
- package/dist/cli.js +103 -38
- package/dist/platform/capabilities.js +53 -0
- package/dist/platform/desktop.js +36 -0
- package/dist/{player/command.js → platform/executables.js} +43 -9
- package/dist/platform/ipc.js +14 -0
- package/dist/platform/launch-command.js +135 -0
- package/dist/platform/loopback.js +44 -0
- package/dist/platform/network.js +312 -0
- package/dist/platform/packages.js +216 -0
- package/dist/platform/paths.js +40 -0
- package/dist/platform/runtime.js +67 -0
- package/dist/platform/shell.js +24 -0
- package/dist/platform/storage.js +48 -0
- package/dist/platform/support.js +214 -0
- package/dist/platform/terminal.js +63 -0
- package/dist/platform/terminals.js +203 -0
- package/dist/player/airplay-discovery.js +4 -2
- package/dist/player/backend-install.js +13 -94
- package/dist/player/command-diagnostics.js +2 -2
- package/dist/player/mpv-ipc-client.js +2 -1
- package/dist/player/player-controller.js +203 -33
- package/dist/providers/cache.js +4 -26
- package/dist/providers/radio-browser.js +152 -54
- package/dist/providers/radio-garden.js +15 -22
- package/dist/setup.js +79 -151
- package/dist/storage/store.js +105 -52
- package/dist/streams/import-stream.js +163 -0
- package/dist/ui/AdaptiveContent.js +33 -24
- package/dist/ui/App.js +173 -86
- package/dist/ui/AppContent.js +3 -3
- package/dist/ui/app-state.js +8 -1
- package/dist/ui/ascii.js +11 -2
- package/dist/ui/components/AdaptiveMarquee.js +6 -3
- package/dist/ui/components/Logo.js +5 -2
- package/dist/ui/components/Menu.js +2 -2
- package/dist/ui/components/ScreenHeader.js +1 -1
- package/dist/ui/components/StationList.js +6 -4
- package/dist/ui/components/TopTabs.js +1 -1
- package/dist/ui/display-context.js +8 -11
- package/dist/ui/help-content.js +5 -5
- package/dist/ui/layout.js +5 -2
- package/dist/ui/page-footer.js +3 -3
- package/dist/ui/screen-items.js +2 -2
- package/dist/ui/screen-meta.js +1 -1
- package/dist/ui/screens/AirPlayCodeScreen.js +6 -2
- package/dist/ui/screens/AirPlaySettingsScreen.js +7 -3
- package/dist/ui/screens/ExploreScreen.js +2 -1
- package/dist/ui/screens/HelpScreen.js +5 -1
- package/dist/ui/screens/HomeScreen.js +5 -1
- package/dist/ui/screens/MapScreen.js +1 -1
- package/dist/ui/screens/NowPlayingScreen.js +4 -4
- package/dist/ui/screens/SearchScreen.js +3 -1
- package/dist/ui/screens/SettingsScreen.js +14 -7
- package/dist/ui/screens/StatsScreen.js +3 -1
- package/dist/ui/system-actions.js +80 -52
- package/dist/ui/terminal-renderer.js +16 -0
- package/dist/ui/use-alarm-tui.js +36 -21
- package/dist/ui/use-app-input.js +42 -22
- package/dist/ui/use-command-executor.js +31 -7
- package/dist/update-check.js +8 -17
- package/package.json +1 -1
package/dist/ui/App.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { Box, Text, useApp, useStdin, useStdout, useWindowSize } from 'ink';
|
|
4
|
+
import { Box, Text, useApp, useIsScreenReaderEnabled, useStdin, useStdout, useWindowSize } from 'ink';
|
|
5
5
|
import { ProviderManager } from '../providers/provider-manager.js';
|
|
6
6
|
import { PlayerController } from '../player/player-controller.js';
|
|
7
|
-
import { playbackBackendInstallHint, playbackBackendLabel } from '../player/backend-install.js';
|
|
7
|
+
import { airPlayMacOSOnlyMessage, isAirPlayPlatformSupported, playbackBackendInstallHint, playbackBackendLabel } from '../player/backend-install.js';
|
|
8
8
|
import { JsonLibraryStore, stationKey } from '../storage/store.js';
|
|
9
9
|
import { defaultAgentControlSettings } from '../types.js';
|
|
10
10
|
import { nextReceiverStyle, nextTheme, textDim, textMuted, themeAccent } from './theme.js';
|
|
@@ -21,8 +21,10 @@ import { useAppInput } from './use-app-input.js';
|
|
|
21
21
|
import { useCommandExecutor } from './use-command-executor.js';
|
|
22
22
|
import { isAirPlayCodePromptActive } from './screens/AirPlayCodeScreen.js';
|
|
23
23
|
import { isAirPlayBackendAvailable } from './airplay-settings.js';
|
|
24
|
+
import { networkPolicy } from '../platform/network.js';
|
|
24
25
|
import { audioOutputLabel, resolvedAudioOutput } from './audio-output.js';
|
|
25
26
|
import { copyToClipboard, openExternal } from './system-actions.js';
|
|
27
|
+
import { safeExternalHttpUrl, safeMediaTarget, sanitizeTerminalText } from '../safety.js';
|
|
26
28
|
import { appVersion } from '../version.js';
|
|
27
29
|
import { automaticUpdateChecksAllowed, checkForUpdate, installUpdate, shouldCheckForUpdate, updateAvailableForVersion, updateCommandForInstall } from '../update-check.js';
|
|
28
30
|
import { helpItemCount } from './help-content.js';
|
|
@@ -37,7 +39,7 @@ import { activeTabForScreen, addMediaKeyBinding, applyStationFilters, clamp, cla
|
|
|
37
39
|
import { ReceiverAnimationProvider } from './receiver-animation.js';
|
|
38
40
|
import { VersionIndicator, versionIndicatorWidth } from './components/VersionIndicator.js';
|
|
39
41
|
const LOADING_SPINNER_MS = 120;
|
|
40
|
-
const
|
|
42
|
+
const TRANSIENT_NOTICE_MS = 4500;
|
|
41
43
|
const LISTENING_HEARTBEAT_MS = 30_000;
|
|
42
44
|
const COUNTRY_STATIONS_PAGE_SIZE = 120;
|
|
43
45
|
const COUNTRY_STATIONS_LOAD_AHEAD = 12;
|
|
@@ -52,15 +54,17 @@ const settingToggleLabel = {
|
|
|
52
54
|
mouseSupport: 'Mouse and trackpad scrolling',
|
|
53
55
|
automaticUpdateChecks: 'Automatic update checks'
|
|
54
56
|
};
|
|
55
|
-
export function App({ store: providedStore, providers: providedProviders, alarmService: providedAlarmService, alarmPreview, initialAgentCommand, mcpConfigurator = configureMcpIntegrations, updateChecker = checkForUpdate }) {
|
|
57
|
+
export function App({ store: providedStore, providers: providedProviders, alarmService: providedAlarmService, alarmPreview, initialAgentCommand, mcpConfigurator = configureMcpIntegrations, updateChecker = checkForUpdate, platform = process.platform }) {
|
|
56
58
|
const { exit } = useApp();
|
|
57
59
|
const { stdin } = useStdin();
|
|
58
60
|
const { stdout } = useStdout();
|
|
59
61
|
const { columns, rows } = useWindowSize();
|
|
62
|
+
const screenReader = useIsScreenReaderEnabled();
|
|
60
63
|
const store = useMemo(() => providedStore ?? new JsonLibraryStore(), [providedStore]);
|
|
61
64
|
const providers = useMemo(() => providedProviders ?? new ProviderManager(), [providedProviders]);
|
|
62
65
|
const alarmService = useMemo(() => providedAlarmService ? serializeAlarmTuiService(providedAlarmService) : createAlarmTuiService(), [providedAlarmService]);
|
|
63
66
|
const installedVersion = useMemo(() => appVersion(), []);
|
|
67
|
+
const airPlaySupported = isAirPlayPlatformSupported(platform);
|
|
64
68
|
const [library, setLibrary] = useState(() => store.snapshot());
|
|
65
69
|
const settingsRef = useRef(library.settings);
|
|
66
70
|
settingsRef.current = library.settings;
|
|
@@ -72,6 +76,8 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
72
76
|
const [selected, setSelected] = useState(0);
|
|
73
77
|
const [settingsPage, setSettingsPage] = useState('root');
|
|
74
78
|
const [message, setMessage] = useState(null);
|
|
79
|
+
const [persistenceWarning, setPersistenceWarning] = useState(null);
|
|
80
|
+
const [presenceWarning, setPresenceWarning] = useState(null);
|
|
75
81
|
const [footerMessage, setFooterMessage] = useState(null);
|
|
76
82
|
const [countries, setCountries] = useState([]);
|
|
77
83
|
const [countryFilter, setCountryFilter] = useState('');
|
|
@@ -95,6 +101,23 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
95
101
|
const [sleepUntil, setSleepUntil] = useState(null);
|
|
96
102
|
const [showDiagnostics, setShowDiagnostics] = useState(false);
|
|
97
103
|
const [capturingTransportAction, setCapturingTransportAction] = useState(null);
|
|
104
|
+
const reportActionError = useCallback((error) => {
|
|
105
|
+
setMessage(sanitizeTerminalText(error instanceof Error ? error.message : String(error)) ?? 'Could not complete this action.');
|
|
106
|
+
}, []);
|
|
107
|
+
// Optional history cannot change the result of playback or navigation. Keep
|
|
108
|
+
// its warning until an explicit save succeeds; a no-op checkpoint proves nothing.
|
|
109
|
+
const persistLibrary = useCallback((write) => {
|
|
110
|
+
try {
|
|
111
|
+
const nextLibrary = write();
|
|
112
|
+
setLibrary(nextLibrary);
|
|
113
|
+
return nextLibrary;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
const detail = sanitizeTerminalText(error instanceof Error ? error.message : String(error));
|
|
117
|
+
setPersistenceWarning(`Library not saved. Set RADIOCLI_HOME to a writable directory.${detail ? ` ${detail}` : ''}`);
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}, []);
|
|
98
121
|
const announcedUpdateRef = useRef(false);
|
|
99
122
|
const automaticUpdateCheckStartedRef = useRef(false);
|
|
100
123
|
const installingUpdateRef = useRef(false);
|
|
@@ -132,8 +155,18 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
132
155
|
const agentHandlerRef = useRef(async () => {
|
|
133
156
|
throw new Error('RadioCLI is still starting.');
|
|
134
157
|
});
|
|
158
|
+
const showTransientMessage = useCallback((nextMessage) => {
|
|
159
|
+
if (transientMessageTimerRef.current) {
|
|
160
|
+
clearTimeout(transientMessageTimerRef.current);
|
|
161
|
+
}
|
|
162
|
+
setMessage(nextMessage);
|
|
163
|
+
transientMessageTimerRef.current = setTimeout(() => {
|
|
164
|
+
setMessage(currentMessage => currentMessage === nextMessage ? null : currentMessage);
|
|
165
|
+
transientMessageTimerRef.current = null;
|
|
166
|
+
}, TRANSIENT_NOTICE_MS);
|
|
167
|
+
}, []);
|
|
135
168
|
const theme = library.settings.theme;
|
|
136
|
-
const displayMode = useMemo(() => resolveDisplayMode(library.settings), [library.settings.transparentBackground, library.settings.asciiMode, library.settings.reduceMotion]);
|
|
169
|
+
const displayMode = useMemo(() => resolveDisplayMode(library.settings, process.env, { screenReader, isTTY: Boolean(stdout.isTTY), colorDepth: stdout.getColorDepth?.() }), [library.settings.transparentBackground, library.settings.asciiMode, library.settings.reduceMotion, screenReader, stdout]);
|
|
137
170
|
const favoriteKeys = useMemo(() => new Set(library.favorites.map(stationKey)), [library.favorites]);
|
|
138
171
|
const diagnostics = player.diagnostics();
|
|
139
172
|
const filterLabel = formatFilterLabel(filters);
|
|
@@ -222,12 +255,31 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
222
255
|
Boolean(isAirPlayBackendAvailable(availableBackends) && selectedAirPlayDevice?.requiresPassword && !selectedAirPlayDevice.local);
|
|
223
256
|
const layout = computeTerminalLayout(columns, rows, footerRows);
|
|
224
257
|
const frameWidth = layout.frameWidth;
|
|
225
|
-
const mouseReportingActive = !
|
|
258
|
+
const mouseReportingActive = !displayMode.screenReader &&
|
|
259
|
+
process.env.TERM?.toLowerCase() !== 'dumb' &&
|
|
260
|
+
!commandMode &&
|
|
226
261
|
!capturingTransportAction &&
|
|
227
262
|
!editingCountryFilter &&
|
|
228
263
|
shouldEnableMouseReporting(screen, itemCountsRef.current[screen] ?? 0, mouseVisibleRows(screen, layout), library.settings.mouseSupport !== false);
|
|
229
264
|
useEffect(() => player.onChange(setPlayback), [player]);
|
|
230
|
-
useEffect(() =>
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
if (!stdin.isTTY)
|
|
267
|
+
return;
|
|
268
|
+
try {
|
|
269
|
+
const unregister = registerTuiPresence();
|
|
270
|
+
return () => {
|
|
271
|
+
try {
|
|
272
|
+
unregister();
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
console.error('RadioCLI could not remove its alarm-control presence marker. Stale markers are checked on the next launch.');
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
setPresenceWarning('Alarm controls cannot register this terminal in the runtime directory. Browsing and playback remain available.');
|
|
281
|
+
}
|
|
282
|
+
}, [stdin]);
|
|
231
283
|
const playingStationRef = useRef(null);
|
|
232
284
|
playingStationRef.current = playingStation;
|
|
233
285
|
const activeListeningStationRef = useRef(null);
|
|
@@ -239,24 +291,24 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
239
291
|
const key = stationKey(station);
|
|
240
292
|
if (activeListeningStationRef.current !== key) {
|
|
241
293
|
activeListeningStationRef.current = key;
|
|
242
|
-
|
|
294
|
+
persistLibrary(() => store.startListeningSession(station));
|
|
243
295
|
}
|
|
244
296
|
return;
|
|
245
297
|
}
|
|
246
298
|
if (activeListeningStationRef.current) {
|
|
247
299
|
activeListeningStationRef.current = null;
|
|
248
|
-
|
|
300
|
+
persistLibrary(() => store.finishActiveListeningSession());
|
|
249
301
|
}
|
|
250
|
-
}, [playback.ready, playback.state, playingStation, store]);
|
|
302
|
+
}, [persistLibrary, playback.ready, playback.state, playingStation, store]);
|
|
251
303
|
useEffect(() => {
|
|
252
304
|
if (playback.state !== 'playing' || !playback.ready || !playingStation) {
|
|
253
305
|
return;
|
|
254
306
|
}
|
|
255
307
|
const timer = setInterval(() => {
|
|
256
|
-
|
|
308
|
+
persistLibrary(() => store.checkpointActiveListeningSession());
|
|
257
309
|
}, LISTENING_HEARTBEAT_MS);
|
|
258
310
|
return () => clearInterval(timer);
|
|
259
|
-
}, [playback.ready, playback.state, playingStation, store]);
|
|
311
|
+
}, [persistLibrary, playback.ready, playback.state, playingStation, store]);
|
|
260
312
|
useEffect(() => player.onMetadata(metadata => {
|
|
261
313
|
setNowPlaying(metadata);
|
|
262
314
|
const station = playingStationRef.current;
|
|
@@ -265,9 +317,9 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
265
317
|
if (lastRecordedTrackRef.current === trackKey)
|
|
266
318
|
return;
|
|
267
319
|
lastRecordedTrackRef.current = trackKey;
|
|
268
|
-
|
|
320
|
+
persistLibrary(() => store.recordTrack(station, metadata.title));
|
|
269
321
|
}
|
|
270
|
-
}), [player, store]);
|
|
322
|
+
}), [persistLibrary, player, store]);
|
|
271
323
|
useEffect(() => {
|
|
272
324
|
if (!stdout.isTTY || !mouseReportingActive) {
|
|
273
325
|
return;
|
|
@@ -285,7 +337,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
285
337
|
}, [renderedStationContextKey, screen, selected]);
|
|
286
338
|
useEffect(() => {
|
|
287
339
|
if (footerPlayback.state !== 'loading' ||
|
|
288
|
-
|
|
340
|
+
displayMode.reduceMotion ||
|
|
289
341
|
process.env.RADIOCLI_DISABLE_ANIMATION === '1' ||
|
|
290
342
|
process.env.RADIO_ATLAS_DISABLE_ANIMATION === '1') {
|
|
291
343
|
setSpinnerFrame(0);
|
|
@@ -293,7 +345,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
293
345
|
}
|
|
294
346
|
const timer = setInterval(() => setSpinnerFrame(value => (value + 1) % 1000), LOADING_SPINNER_MS);
|
|
295
347
|
return () => clearInterval(timer);
|
|
296
|
-
}, [footerPlayback.state,
|
|
348
|
+
}, [footerPlayback.state, displayMode.reduceMotion]);
|
|
297
349
|
useEffect(() => {
|
|
298
350
|
if ((screen === 'countries' || screen === 'map') &&
|
|
299
351
|
countries.length === 0 &&
|
|
@@ -332,27 +384,30 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
332
384
|
const delayMs = sleepUntil - Date.now();
|
|
333
385
|
if (delayMs <= 0) {
|
|
334
386
|
tuneRequestRef.current += 1;
|
|
335
|
-
|
|
336
|
-
void player.stop();
|
|
387
|
+
persistLibrary(() => store.finishActiveListeningSession());
|
|
388
|
+
void player.stop().catch(reportActionError);
|
|
337
389
|
setSleepUntil(null);
|
|
338
390
|
return;
|
|
339
391
|
}
|
|
340
392
|
const timer = setTimeout(() => {
|
|
341
393
|
tuneRequestRef.current += 1;
|
|
342
|
-
|
|
343
|
-
void player.stop();
|
|
394
|
+
persistLibrary(() => store.finishActiveListeningSession());
|
|
395
|
+
void player.stop().catch(reportActionError);
|
|
344
396
|
setSleepUntil(null);
|
|
345
397
|
}, delayMs);
|
|
346
398
|
return () => clearTimeout(timer);
|
|
347
|
-
}, [player, sleepUntil, store]);
|
|
399
|
+
}, [persistLibrary, player, reportActionError, sleepUntil, store]);
|
|
348
400
|
useEffect(() => {
|
|
349
401
|
const backends = player.refreshDetectedBackends();
|
|
350
402
|
setAvailableBackends(backends);
|
|
351
|
-
|
|
403
|
+
const network = networkPolicy();
|
|
404
|
+
if (airPlaySupported && !network.offline && !network.lowBandwidth) {
|
|
405
|
+
void player.refreshAirPlayDevices().then(setAvailableAirPlayDevices).catch(() => setAvailableAirPlayDevices([]));
|
|
406
|
+
}
|
|
352
407
|
if (backends.length === 0) {
|
|
353
408
|
setMessage(`No playback backend found. ${playbackBackendInstallHint()}`);
|
|
354
409
|
}
|
|
355
|
-
}, [player]);
|
|
410
|
+
}, [airPlaySupported, player]);
|
|
356
411
|
const refreshProviderHealth = useCallback(() => {
|
|
357
412
|
providers.health(settingsRef.current).then(setProviderHealth).catch(() => setProviderHealth({}));
|
|
358
413
|
}, [providers]);
|
|
@@ -361,9 +416,9 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
361
416
|
}, [refreshProviderHealth]);
|
|
362
417
|
const refreshUpdateCheck = useCallback(async () => {
|
|
363
418
|
const updateCheck = await updateChecker({ currentVersion: installedVersion });
|
|
364
|
-
|
|
419
|
+
persistLibrary(() => store.updateCheckState(updateCheck));
|
|
365
420
|
return updateCheck;
|
|
366
|
-
}, [installedVersion, store, updateChecker]);
|
|
421
|
+
}, [installedVersion, persistLibrary, store, updateChecker]);
|
|
367
422
|
useEffect(() => {
|
|
368
423
|
if (automaticUpdateCheckStartedRef.current ||
|
|
369
424
|
!automaticUpdateChecksAllowed(library.settings.automaticUpdateChecks !== false)) {
|
|
@@ -379,11 +434,14 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
379
434
|
announcedUpdateRef.current = true;
|
|
380
435
|
setMessage(`Update available: v${updateCheck.latestVersion} · run :update`);
|
|
381
436
|
}
|
|
437
|
+
}).catch(error => {
|
|
438
|
+
if (!cancelled)
|
|
439
|
+
reportActionError(error);
|
|
382
440
|
});
|
|
383
441
|
return () => {
|
|
384
442
|
cancelled = true;
|
|
385
443
|
};
|
|
386
|
-
}, [library.settings.automaticUpdateChecks, refreshUpdateCheck]);
|
|
444
|
+
}, [library.settings.automaticUpdateChecks, refreshUpdateCheck, reportActionError]);
|
|
387
445
|
const setStationContextFor = useCallback((key, context) => {
|
|
388
446
|
setStationContexts(current => ({ ...current, [key]: context }));
|
|
389
447
|
}, []);
|
|
@@ -393,6 +451,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
393
451
|
const nextLibrary = store.updateSettings(settings);
|
|
394
452
|
settingsRef.current = nextLibrary.settings;
|
|
395
453
|
setLibrary(nextLibrary);
|
|
454
|
+
setPersistenceWarning(null);
|
|
396
455
|
return nextLibrary;
|
|
397
456
|
}, [store]);
|
|
398
457
|
useEffect(() => {
|
|
@@ -404,9 +463,14 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
404
463
|
setMessage(`${selectedAirPlayDevice.name} is this Mac. Install mpv to use local playback instead of AirPlay.`);
|
|
405
464
|
return;
|
|
406
465
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
466
|
+
try {
|
|
467
|
+
updateSettings({ preferredBackend });
|
|
468
|
+
setMessage(`${selectedAirPlayDevice.name} is this Mac. Audio output: ${audioOutputLabel(preferredBackend)}.`);
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
reportActionError(error);
|
|
472
|
+
}
|
|
473
|
+
}, [availableBackends, reportActionError, selectedAirPlayDevice, updateSettings]);
|
|
410
474
|
const updateMediaKeys = useCallback((mediaKeys) => {
|
|
411
475
|
updateSettings({ mediaKeys: normalizeMediaKeyBindings(mediaKeys) });
|
|
412
476
|
}, [updateSettings]);
|
|
@@ -466,9 +530,13 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
466
530
|
openSettingsPage('root');
|
|
467
531
|
}, [go, openSettingsPage]);
|
|
468
532
|
const openAirPlayCode = useCallback(() => {
|
|
533
|
+
if (!airPlaySupported) {
|
|
534
|
+
showTransientMessage(airPlayMacOSOnlyMessage);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
469
537
|
setAirPlayCode('');
|
|
470
538
|
go('airplay-code', { resetSelection: true, clearMessage: false });
|
|
471
|
-
}, [go]);
|
|
539
|
+
}, [airPlaySupported, go, showTransientMessage]);
|
|
472
540
|
useEffect(() => {
|
|
473
541
|
if (isAirPlayCodePromptActive(playback) && screenRef.current !== 'airplay-code') {
|
|
474
542
|
openAirPlayCode();
|
|
@@ -476,13 +544,13 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
476
544
|
}, [openAirPlayCode, playback.backend, playback.message]);
|
|
477
545
|
const shutdown = useCallback(() => {
|
|
478
546
|
tuneRequestRef.current += 1;
|
|
479
|
-
store.finishActiveListeningSession();
|
|
480
|
-
player.stop().finally(exit);
|
|
481
|
-
}, [exit, player, store]);
|
|
547
|
+
persistLibrary(() => store.finishActiveListeningSession());
|
|
548
|
+
void player.stop().catch(reportActionError).finally(exit);
|
|
549
|
+
}, [exit, persistLibrary, player, reportActionError, store]);
|
|
482
550
|
useEffect(() => {
|
|
483
551
|
const finishForSignal = () => {
|
|
484
|
-
store.finishActiveListeningSession();
|
|
485
|
-
void player.stop().finally(() => process.exit(0));
|
|
552
|
+
persistLibrary(() => store.finishActiveListeningSession());
|
|
553
|
+
void player.stop().catch(reportActionError).finally(() => process.exit(0));
|
|
486
554
|
};
|
|
487
555
|
process.once('SIGTERM', finishForSignal);
|
|
488
556
|
process.once('SIGHUP', finishForSignal);
|
|
@@ -490,7 +558,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
490
558
|
process.off('SIGTERM', finishForSignal);
|
|
491
559
|
process.off('SIGHUP', finishForSignal);
|
|
492
560
|
};
|
|
493
|
-
}, [player, store]);
|
|
561
|
+
}, [persistLibrary, player, reportActionError, store]);
|
|
494
562
|
const showStationContext = useCallback((context, next = 'stations', options = {}) => {
|
|
495
563
|
setStationContextFor(stationContextKeyForScreen(next) ?? 'stations', context);
|
|
496
564
|
go(next, { resetSelection: options.resetSelection ?? true, clearMessage: options.clearMessage });
|
|
@@ -692,7 +760,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
692
760
|
});
|
|
693
761
|
selectedByScreenRef.current.search = 0;
|
|
694
762
|
lastSubmittedSearchRef.current = query.trim();
|
|
695
|
-
|
|
763
|
+
persistLibrary(() => store.addSearch(query));
|
|
696
764
|
searchHistoryRef.current = { cursor: -1, draft: '' };
|
|
697
765
|
setSelected(0);
|
|
698
766
|
setEditingSearch(true);
|
|
@@ -706,7 +774,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
706
774
|
if (requestId === searchRequestRef.current)
|
|
707
775
|
setLoadingStations(false);
|
|
708
776
|
}
|
|
709
|
-
}, [filters, providers, searchQuery, setStationContextFor, store]);
|
|
777
|
+
}, [filters, persistLibrary, providers, searchQuery, setStationContextFor, store]);
|
|
710
778
|
const loadMoreSearchResults = useCallback(async () => {
|
|
711
779
|
const context = stationContextsRef.current.search;
|
|
712
780
|
const query = context.query;
|
|
@@ -911,8 +979,8 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
911
979
|
setPlayingStation(station);
|
|
912
980
|
setTuningStation(null);
|
|
913
981
|
playbackQueueRef.current = queue;
|
|
914
|
-
const nextLibrary = store.addRecent(station);
|
|
915
|
-
if (screenRef.current === 'library') {
|
|
982
|
+
const nextLibrary = persistLibrary(() => store.addRecent(station));
|
|
983
|
+
if (nextLibrary && screenRef.current === 'library') {
|
|
916
984
|
const nextLibraryStations = applyStationFilters(buildLibraryStations(nextLibrary), filters);
|
|
917
985
|
const nextLibraryIndex = nextLibraryStations.findIndex(item => stationMatches(item, station));
|
|
918
986
|
if (nextLibraryIndex >= 0) {
|
|
@@ -920,7 +988,6 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
920
988
|
setSelected(nextLibraryIndex);
|
|
921
989
|
}
|
|
922
990
|
}
|
|
923
|
-
setLibrary(nextLibrary);
|
|
924
991
|
if (options.openNowPlaying) {
|
|
925
992
|
go('now-playing');
|
|
926
993
|
}
|
|
@@ -946,7 +1013,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
946
1013
|
setTuningStation(null);
|
|
947
1014
|
setMessage(message);
|
|
948
1015
|
}
|
|
949
|
-
}, [filters, go, player, providers, queueFromCurrentList, rememberQueueSelection, stationMatches, store]);
|
|
1016
|
+
}, [filters, go, persistLibrary, player, providers, queueFromCurrentList, rememberQueueSelection, stationMatches, store]);
|
|
950
1017
|
// Resume the most recent station on launch when opted in, like a radio
|
|
951
1018
|
// powering back on to its last frequency. Runs once and never auto-navigates.
|
|
952
1019
|
const didResumeRef = useRef(false);
|
|
@@ -972,7 +1039,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
972
1039
|
setTuningStation(null);
|
|
973
1040
|
setMessage('Auto-skip canceled. Choose a station and press Enter.');
|
|
974
1041
|
}, []);
|
|
975
|
-
const showTransientFooterMessage = useCallback((nextMessage, durationMs =
|
|
1042
|
+
const showTransientFooterMessage = useCallback((nextMessage, durationMs = TRANSIENT_NOTICE_MS) => {
|
|
976
1043
|
if (transientFooterMessageTimerRef.current) {
|
|
977
1044
|
clearTimeout(transientFooterMessageTimerRef.current);
|
|
978
1045
|
}
|
|
@@ -998,31 +1065,37 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
998
1065
|
}
|
|
999
1066
|
const wasFavorite = store.isFavorite(station);
|
|
1000
1067
|
setLibrary(store.toggleFavorite(station));
|
|
1068
|
+
setPersistenceWarning(null);
|
|
1001
1069
|
const favoriteMessage = `${wasFavorite ? 'Removed from' : 'Added to'} favorites: ${station.name}`;
|
|
1002
|
-
if (screenRef.current === 'library') {
|
|
1070
|
+
if (screenRef.current === 'library' || screenRef.current === 'search') {
|
|
1003
1071
|
showTransientFooterMessage(favoriteMessage);
|
|
1004
1072
|
}
|
|
1005
1073
|
else {
|
|
1006
|
-
|
|
1074
|
+
showTransientMessage(favoriteMessage);
|
|
1007
1075
|
}
|
|
1008
1076
|
if (!wasFavorite && settingsRef.current.shareDirectoryVotes) {
|
|
1009
1077
|
// Best-effort upvote back to the directory; never blocks favoriting.
|
|
1010
|
-
void providers.vote(station);
|
|
1078
|
+
void providers.vote(station).catch(reportActionError);
|
|
1011
1079
|
}
|
|
1012
|
-
}, [providers, showTransientFooterMessage, store]);
|
|
1080
|
+
}, [providers, reportActionError, showTransientFooterMessage, showTransientMessage, store]);
|
|
1013
1081
|
const showControlResult = useCallback((result) => {
|
|
1014
1082
|
if (!result.ok && result.message) {
|
|
1015
1083
|
setMessage(result.message);
|
|
1016
1084
|
}
|
|
1017
1085
|
}, []);
|
|
1018
|
-
const openStationHomepage = useCallback((station) => {
|
|
1086
|
+
const openStationHomepage = useCallback(async (station) => {
|
|
1019
1087
|
if (!station?.homepage) {
|
|
1020
1088
|
setMessage('This station has no homepage.');
|
|
1021
1089
|
return;
|
|
1022
1090
|
}
|
|
1023
|
-
|
|
1091
|
+
const url = safeExternalHttpUrl(station.homepage);
|
|
1092
|
+
if (!url) {
|
|
1093
|
+
setMessage('This station has no valid HTTP(S) homepage.');
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
setMessage(await openExternal(url)
|
|
1024
1097
|
? `Opening homepage: ${station.name}`
|
|
1025
|
-
:
|
|
1098
|
+
: `Could not open a browser in this session. Homepage: ${sanitizeTerminalText(url) ?? ''}`);
|
|
1026
1099
|
}, []);
|
|
1027
1100
|
const copyStationUrl = useCallback(async (station) => {
|
|
1028
1101
|
if (!station) {
|
|
@@ -1033,12 +1106,13 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1033
1106
|
if (!url) {
|
|
1034
1107
|
url = await providers.resolve(station).then(resolved => resolved.url).catch(() => undefined);
|
|
1035
1108
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1109
|
+
const safeUrl = url ? safeMediaTarget(url) : null;
|
|
1110
|
+
if (!safeUrl) {
|
|
1111
|
+
setMessage(`No safe stream URL available for ${station.name}.`);
|
|
1038
1112
|
return;
|
|
1039
1113
|
}
|
|
1040
|
-
const copied = copyToClipboard(
|
|
1041
|
-
setMessage(copied ? `Copied stream URL: ${station.name}` : `Stream URL: ${
|
|
1114
|
+
const copied = await copyToClipboard(safeUrl);
|
|
1115
|
+
setMessage(copied ? `Copied stream URL: ${station.name}` : `Stream URL: ${sanitizeTerminalText(safeUrl) ?? ''}`);
|
|
1042
1116
|
}, [providers]);
|
|
1043
1117
|
const submitAirPlayCode = useCallback((code) => {
|
|
1044
1118
|
const result = player.submitAirPlayPasscode(code);
|
|
@@ -1059,8 +1133,8 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1059
1133
|
updateSettings({ volume: player.getState().volume });
|
|
1060
1134
|
}
|
|
1061
1135
|
showControlResult(result);
|
|
1062
|
-
});
|
|
1063
|
-
}, [player, showControlResult, updateSettings]);
|
|
1136
|
+
}).catch(reportActionError);
|
|
1137
|
+
}, [player, reportActionError, showControlResult, updateSettings]);
|
|
1064
1138
|
const adjustVolume = useCallback((delta) => {
|
|
1065
1139
|
const requestId = volumeRequestRef.current + 1;
|
|
1066
1140
|
volumeRequestRef.current = requestId;
|
|
@@ -1069,24 +1143,14 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1069
1143
|
updateSettings({ volume: player.getState().volume });
|
|
1070
1144
|
}
|
|
1071
1145
|
showControlResult(result);
|
|
1072
|
-
});
|
|
1073
|
-
}, [player, showControlResult, updateSettings]);
|
|
1146
|
+
}).catch(reportActionError);
|
|
1147
|
+
}, [player, reportActionError, showControlResult, updateSettings]);
|
|
1074
1148
|
const toggleMute = useCallback(() => {
|
|
1075
|
-
void player.toggleMute().then(showControlResult);
|
|
1076
|
-
}, [player, showControlResult]);
|
|
1149
|
+
void player.toggleMute().then(showControlResult).catch(reportActionError);
|
|
1150
|
+
}, [player, reportActionError, showControlResult]);
|
|
1077
1151
|
const togglePause = useCallback(() => {
|
|
1078
|
-
void player.togglePause().then(showControlResult);
|
|
1079
|
-
}, [player, showControlResult]);
|
|
1080
|
-
const showTransientMessage = useCallback((nextMessage) => {
|
|
1081
|
-
if (transientMessageTimerRef.current) {
|
|
1082
|
-
clearTimeout(transientMessageTimerRef.current);
|
|
1083
|
-
}
|
|
1084
|
-
setMessage(nextMessage);
|
|
1085
|
-
transientMessageTimerRef.current = setTimeout(() => {
|
|
1086
|
-
setMessage(currentMessage => currentMessage === nextMessage ? null : currentMessage);
|
|
1087
|
-
transientMessageTimerRef.current = null;
|
|
1088
|
-
}, VISUALIZER_MESSAGE_MS);
|
|
1089
|
-
}, []);
|
|
1152
|
+
void player.togglePause().then(showControlResult).catch(reportActionError);
|
|
1153
|
+
}, [player, reportActionError, showControlResult]);
|
|
1090
1154
|
const cycleDisplayColor = useCallback(() => {
|
|
1091
1155
|
const theme = nextTheme(settingsRef.current.theme);
|
|
1092
1156
|
updateSettings({ theme });
|
|
@@ -1125,6 +1189,15 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1125
1189
|
setMessage(`Radio Browser favorite votes ${shareDirectoryVotes ? 'enabled' : 'disabled'}.`);
|
|
1126
1190
|
}, [updateSettings]);
|
|
1127
1191
|
const refreshAirPlayTargets = useCallback(async (announce = true) => {
|
|
1192
|
+
if (!airPlaySupported) {
|
|
1193
|
+
if (announce)
|
|
1194
|
+
showTransientMessage(airPlayMacOSOnlyMessage);
|
|
1195
|
+
return [];
|
|
1196
|
+
}
|
|
1197
|
+
if (networkPolicy().offline) {
|
|
1198
|
+
setMessage('AirPlay discovery is disabled by RADIOCLI_OFFLINE=1.');
|
|
1199
|
+
return [];
|
|
1200
|
+
}
|
|
1128
1201
|
try {
|
|
1129
1202
|
const devices = await player.refreshAirPlayDevices();
|
|
1130
1203
|
setAvailableAirPlayDevices(devices);
|
|
@@ -1147,13 +1220,17 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1147
1220
|
}
|
|
1148
1221
|
return [];
|
|
1149
1222
|
}
|
|
1150
|
-
}, [player]);
|
|
1223
|
+
}, [airPlaySupported, player, showTransientMessage]);
|
|
1151
1224
|
const openAirPlaySettings = useCallback(() => {
|
|
1225
|
+
if (!airPlaySupported) {
|
|
1226
|
+
showTransientMessage(airPlayMacOSOnlyMessage);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1152
1229
|
const preferredIndex = availableAirPlayDevices.findIndex(device => device.id === settingsRef.current.preferredAirPlayDevice);
|
|
1153
1230
|
selectedByScreenRef.current['airplay-settings'] = Math.max(0, preferredIndex);
|
|
1154
1231
|
go('airplay-settings', { resetSelection: false });
|
|
1155
1232
|
void refreshAirPlayTargets(false);
|
|
1156
|
-
}, [availableAirPlayDevices, go, refreshAirPlayTargets]);
|
|
1233
|
+
}, [airPlaySupported, availableAirPlayDevices, go, refreshAirPlayTargets, showTransientMessage]);
|
|
1157
1234
|
const selectAirPlayDeviceAt = useCallback((index) => {
|
|
1158
1235
|
const device = availableAirPlayDevices[index];
|
|
1159
1236
|
if (!device) {
|
|
@@ -1357,7 +1434,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1357
1434
|
}
|
|
1358
1435
|
}
|
|
1359
1436
|
const command = updateCommandForInstall();
|
|
1360
|
-
const copied = copyToClipboard(command.command);
|
|
1437
|
+
const copied = await copyToClipboard(command.command);
|
|
1361
1438
|
const latestVersion = store.snapshot().updateCheck?.latestVersion ?? updateCheck?.latestVersion;
|
|
1362
1439
|
const prefix = latestVersion ? `Latest v${latestVersion}. ` : '';
|
|
1363
1440
|
const method = command.method === 'homebrew' ? 'Homebrew' : command.method === 'npm' ? 'npm' : 'your install method';
|
|
@@ -1415,6 +1492,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1415
1492
|
loadCountry,
|
|
1416
1493
|
openAirPlaySettings,
|
|
1417
1494
|
openLibrary,
|
|
1495
|
+
persistLibrary,
|
|
1418
1496
|
player,
|
|
1419
1497
|
playingStation,
|
|
1420
1498
|
providers,
|
|
@@ -1471,6 +1549,9 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1471
1549
|
}
|
|
1472
1550
|
});
|
|
1473
1551
|
const respond = (message, ok = true, data) => ({ ok, message, status: sessionStatus(), ...(data ? { data } : {}) });
|
|
1552
|
+
if (['airplay-list', 'airplay-select', 'airplay-passcode'].includes(command.type) && !airPlaySupported) {
|
|
1553
|
+
return respond(airPlayMacOSOnlyMessage, false, command.type === 'airplay-list' ? [] : undefined);
|
|
1554
|
+
}
|
|
1474
1555
|
if (command.type === 'status')
|
|
1475
1556
|
return respond(playingStationRef.current ? `${player.getState().state}: ${playingStationRef.current.name}` : 'RadioCLI is idle.');
|
|
1476
1557
|
if (command.type === 'play') {
|
|
@@ -1498,7 +1579,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1498
1579
|
}
|
|
1499
1580
|
if (command.type === 'stop') {
|
|
1500
1581
|
tuneRequestRef.current += 1;
|
|
1501
|
-
|
|
1582
|
+
persistLibrary(() => store.finishActiveListeningSession());
|
|
1502
1583
|
await player.stop();
|
|
1503
1584
|
playingStationRef.current = null;
|
|
1504
1585
|
setPlayingStation(null);
|
|
@@ -1508,7 +1589,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1508
1589
|
}
|
|
1509
1590
|
if (command.type === 'alarm-preempt') {
|
|
1510
1591
|
tuneRequestRef.current += 1;
|
|
1511
|
-
|
|
1592
|
+
persistLibrary(() => store.finishActiveListeningSession());
|
|
1512
1593
|
await player.stop();
|
|
1513
1594
|
playingStationRef.current = null;
|
|
1514
1595
|
setPlayingStation(null);
|
|
@@ -1536,10 +1617,14 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1536
1617
|
return respond(`${command.favorite ? 'Favorited' : 'Removed favorite'}: ${station.name}.`);
|
|
1537
1618
|
}
|
|
1538
1619
|
if (command.type === 'airplay-list') {
|
|
1620
|
+
if (networkPolicy().offline)
|
|
1621
|
+
return respond('AirPlay discovery is disabled by RADIOCLI_OFFLINE=1.', false, []);
|
|
1539
1622
|
const devices = await refreshAirPlayTargets(false);
|
|
1540
1623
|
return respond(devices.length ? `${devices.length} AirPlay receiver(s) found.` : 'No AirPlay receivers found.', true, devices);
|
|
1541
1624
|
}
|
|
1542
1625
|
if (command.type === 'airplay-select') {
|
|
1626
|
+
if (networkPolicy().offline)
|
|
1627
|
+
return respond('AirPlay discovery is disabled by RADIOCLI_OFFLINE=1.', false);
|
|
1543
1628
|
const devices = await refreshAirPlayTargets(false);
|
|
1544
1629
|
const device = devices.find(item => item.id === command.deviceId);
|
|
1545
1630
|
if (!device)
|
|
@@ -1711,6 +1796,7 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1711
1796
|
spinnerFrame
|
|
1712
1797
|
});
|
|
1713
1798
|
const basePageFooter = pageFooterText({
|
|
1799
|
+
airPlaySupported,
|
|
1714
1800
|
canEnterAirPlayCode,
|
|
1715
1801
|
capturingTransportAction,
|
|
1716
1802
|
commandMode,
|
|
@@ -1730,7 +1816,8 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1730
1816
|
(screen === 'search' && editingSearch) ||
|
|
1731
1817
|
screen === 'airplay-code');
|
|
1732
1818
|
const hasActiveMicroPlayback = Boolean(footerStation && (footerPlayback.state === 'playing' || footerPlayback.state === 'paused'));
|
|
1733
|
-
const
|
|
1819
|
+
const statusMessage = message ?? persistenceWarning ?? presenceWarning;
|
|
1820
|
+
const microFooter = footerMessage ?? statusMessage ?? (pageFooterOwnsCompactRow
|
|
1734
1821
|
? pageFooter
|
|
1735
1822
|
: footerPlayback.state === 'loading' && playbackFooter
|
|
1736
1823
|
? playbackFooter
|
|
@@ -1739,17 +1826,17 @@ export function App({ store: providedStore, providers: providedProviders, alarmS
|
|
|
1739
1826
|
: pageFooter);
|
|
1740
1827
|
const compactGlobalFooter = '←/→ tabs · ? help · q quit';
|
|
1741
1828
|
const versionReserve = versionIndicatorWidth(installedVersion, library.updateCheck) + 2;
|
|
1742
|
-
const fullStatusRows = fullStatusFooterRows(screen,
|
|
1829
|
+
const fullStatusRows = fullStatusFooterRows(screen, statusMessage, footerMessage, playbackFooter);
|
|
1743
1830
|
const fullLegendRows = balancedFooterLegendRows(pageFooter, globalFooter, frameWidth, 2, versionReserve);
|
|
1744
|
-
const compactStatus = footerMessage ??
|
|
1831
|
+
const compactStatus = footerMessage ?? statusMessage ?? playbackFooter;
|
|
1745
1832
|
const compactLegendRowCount = Math.max(1, layout.footerRows - (compactStatus ? 1 : 0));
|
|
1746
1833
|
const compactLegendRows = balancedFooterLegendRows(pageFooter, compactGlobalFooter, frameWidth, compactLegendRowCount, versionReserve);
|
|
1747
1834
|
const microLegendWidth = Math.max(1, frameWidth - versionReserve);
|
|
1748
|
-
const microLegend = commandMode || capturingTransportAction || footerMessage ||
|
|
1835
|
+
const microLegend = commandMode || capturingTransportAction || footerMessage || statusMessage
|
|
1749
1836
|
? microFooter
|
|
1750
1837
|
: microShortcutFooterText(microFooter, microLegendWidth);
|
|
1751
1838
|
const footerText = (value) => displayMode.ascii ? toAsciiSafe(value) : value;
|
|
1752
|
-
return (_jsx(ReceiverAnimationProvider, { screen: screen, playback: playback, receiverStyle: library.settings.receiverStyle, reduceMotion:
|
|
1839
|
+
return (_jsx(ReceiverAnimationProvider, { screen: screen, playback: playback, receiverStyle: library.settings.receiverStyle, reduceMotion: displayMode.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, airPlaySupported: airPlaySupported, 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 || statusMessage ? 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 || statusMessage ? 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
1840
|
}
|
|
1754
1841
|
function mcpRuntime() {
|
|
1755
1842
|
return { nodePath: process.execPath, cliPath: fileURLToPath(new URL('../cli.js', import.meta.url)) };
|
|
@@ -1777,12 +1864,12 @@ function buildLibraryStations(library) {
|
|
|
1777
1864
|
for (const station of library.favorites) {
|
|
1778
1865
|
addStation(station);
|
|
1779
1866
|
}
|
|
1780
|
-
for (const item of library.recent) {
|
|
1781
|
-
addStation(item.station);
|
|
1782
|
-
}
|
|
1783
1867
|
for (const station of library.imported) {
|
|
1784
1868
|
addStation(station);
|
|
1785
1869
|
}
|
|
1870
|
+
for (const item of library.recent) {
|
|
1871
|
+
addStation(item.station);
|
|
1872
|
+
}
|
|
1786
1873
|
return stations;
|
|
1787
1874
|
}
|
|
1788
1875
|
function exploreCursorLocation(cursor) {
|
|
@@ -1880,7 +1967,7 @@ function audioOutputSwitchLabel(output, backends) {
|
|
|
1880
1967
|
return audioOutputLabel(output);
|
|
1881
1968
|
}
|
|
1882
1969
|
function librarySubtitle(library) {
|
|
1883
|
-
return `${library.favorites.length} favorites · ${library.
|
|
1970
|
+
return `${library.favorites.length} favorites · ${library.imported.length} imported · ${library.recent.length} recent · new imports before recents`;
|
|
1884
1971
|
}
|
|
1885
1972
|
function mouseVisibleRows(screen, layout) {
|
|
1886
1973
|
if (screen === 'help')
|