@ciphore/radiocli 0.2.1 → 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 +133 -1
- package/README.md +76 -6
- 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/active-session.js +183 -0
- package/dist/alarms/cli.js +312 -0
- package/dist/alarms/guard.js +343 -0
- package/dist/alarms/inhibitor.js +48 -0
- package/dist/alarms/power-guard-store.js +169 -0
- package/dist/alarms/runner.js +342 -0
- package/dist/alarms/runtime-health.js +79 -0
- package/dist/alarms/schedule.js +149 -0
- package/dist/alarms/scheduler.js +250 -0
- package/dist/alarms/setup-verification.js +187 -0
- package/dist/alarms/system-volume.js +43 -0
- package/dist/alarms/terminal-launcher.js +181 -0
- package/dist/alarms/tui-presence.js +38 -0
- package/dist/cli.js +113 -5
- package/dist/player/backend-install.js +2 -1
- package/dist/player/command-diagnostics.js +27 -0
- package/dist/player/command.js +123 -62
- package/dist/player/player-controller.js +32 -2
- package/dist/providers/provider-manager.js +5 -0
- package/dist/providers/radio-browser.js +36 -6
- package/dist/setup.js +462 -0
- package/dist/storage/store.js +262 -2
- package/dist/types.js +6 -0
- package/dist/ui/AdaptiveContent.js +111 -26
- package/dist/ui/App.js +401 -67
- package/dist/ui/AppContent.js +24 -7
- package/dist/ui/adaptive-explore-layout.js +47 -0
- package/dist/ui/alarm-editor.js +174 -0
- package/dist/ui/alarm-tui-service.js +84 -0
- package/dist/ui/app-state.js +3 -0
- package/dist/ui/ascii.js +8 -0
- package/dist/ui/components/AdaptiveMarquee.js +28 -0
- package/dist/ui/components/StationList.js +10 -5
- package/dist/ui/components/VersionIndicator.js +19 -0
- package/dist/ui/cosmo-world-map.js +5 -2
- package/dist/ui/explore-map-layout.js +18 -6
- package/dist/ui/format.js +21 -0
- package/dist/ui/help-content.js +14 -2
- package/dist/ui/layout.js +1 -1
- package/dist/ui/page-footer.js +120 -2
- package/dist/ui/receiver-animation.js +68 -0
- package/dist/ui/screen-items.js +41 -9
- package/dist/ui/screen-meta.js +4 -0
- package/dist/ui/screens/AlarmsScreen.js +202 -0
- package/dist/ui/screens/CountriesScreen.js +8 -5
- package/dist/ui/screens/ExploreScreen.js +8 -3
- package/dist/ui/screens/HomeScreen.js +3 -1
- package/dist/ui/screens/NowPlayingScreen.js +6 -2
- package/dist/ui/screens/SettingsScreen.js +70 -53
- package/dist/ui/screens/StationScreen.js +3 -2
- package/dist/ui/selection-state.js +10 -0
- package/dist/ui/terminal-mouse.js +18 -3
- package/dist/ui/use-alarm-tui.js +727 -0
- package/dist/ui/use-app-input.js +107 -46
- 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 +14 -7
- package/dist/ui/visualizers/receiver-visualizers.js +233 -128
- 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 +6 -1
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
2
3
|
import { Box, Text } from 'ink';
|
|
3
4
|
import { computeListeningStats } from '../activity/stats.js';
|
|
4
5
|
import { playbackBackendLabel } from '../player/backend-install.js';
|
|
5
6
|
import { visibleWindow } from './list-window.js';
|
|
6
7
|
import { displayWidth, padDisplayEnd, stationLocation, stationTags, stationTech, truncate } from './format.js';
|
|
7
|
-
import { homeItems,
|
|
8
|
+
import { homeItems, settingsGroup, settingsGroups, settingsItemsForPage } from './screen-items.js';
|
|
8
9
|
import { screenTitle } from './screen-meta.js';
|
|
9
10
|
import { keyHelpSections, commandHelp } from './help-content.js';
|
|
10
11
|
import { settingLabel, settingValue } from './screens/SettingsScreen.js';
|
|
11
|
-
import { panelBorder, textDim, textMuted, themeAccent } from './theme.js';
|
|
12
|
+
import { exploreMapLand, mapMarker, panelBorder, textDim, textMuted, themeAccent } from './theme.js';
|
|
12
13
|
import { panelBorderStyle, useDisplay } from './display-context.js';
|
|
13
14
|
import { toAsciiSafe } from './ascii.js';
|
|
14
15
|
import { buildVisualizer, visualizerHeight } from './visualizers/receiver-visualizers.js';
|
|
15
16
|
import { Logo } from './components/Logo.js';
|
|
17
|
+
import { useReceiverPulse } from './receiver-animation.js';
|
|
18
|
+
import { formatExploreCursor } from './app-state.js';
|
|
19
|
+
import { buildCosmoWorldMap } from './cosmo-world-map.js';
|
|
20
|
+
import { adaptiveExploreFrameMetrics, computeAdaptiveExploreLayout } from './adaptive-explore-layout.js';
|
|
21
|
+
import { AdaptiveMarquee } from './components/AdaptiveMarquee.js';
|
|
16
22
|
export function AdaptiveContent(props) {
|
|
17
23
|
return _jsx(AdaptiveContentBody, { ...props });
|
|
18
24
|
}
|
|
19
25
|
function AdaptiveContentBody(props) {
|
|
20
|
-
const { mode, screen, selected, height, width, theme, playback, playingStation, nowPlaying, stations, countries, airPlayDevices, airPlayCode, searchQuery, editingSearch, countryFilter, loadingCountries, loadingStations, library, diagnostics, backends, updateCheck, favoriteKeys, stationTitle,
|
|
26
|
+
const { mode, screen, settingsPage = 'root', selected, height, width, theme, playback, playingStation, nowPlaying, stations, countries, airPlayDevices, airPlayCode, searchQuery, editingSearch, countryFilter, loadingCountries, loadingStations, exploreCursor, library, diagnostics, backends, updateCheck, appVersion, favoriteKeys, stationTitle, } = props;
|
|
21
27
|
const accent = themeAccent(theme);
|
|
22
28
|
const { ascii } = useDisplay();
|
|
23
29
|
const { bodyRows } = adaptiveFrameMetrics(mode, height);
|
|
@@ -27,11 +33,15 @@ function AdaptiveContentBody(props) {
|
|
|
27
33
|
return (_jsx(AdaptiveHome, { mode: mode, selected: selected, height: height, width: width, theme: theme, library: library }));
|
|
28
34
|
}
|
|
29
35
|
if (screen === 'now-playing') {
|
|
30
|
-
return (_jsx(AdaptiveNowPlaying, { mode: mode, station: playingStation, playback: playback, metadata: nowPlaying,
|
|
36
|
+
return (_jsx(AdaptiveNowPlaying, { mode: mode, station: playingStation, playback: playback, metadata: nowPlaying, width: width, height: height, ascii: ascii, theme: theme, receiverStyle: library.settings.receiverStyle, reduceMotion: Boolean(library.settings.reduceMotion) }));
|
|
37
|
+
}
|
|
38
|
+
if (screen === 'explore') {
|
|
39
|
+
return (_jsx(AdaptiveExplore, { mode: mode, width: width, height: height, theme: theme, stations: stations, selected: selected, favorites: favoriteKeys, cursor: exploreCursor, loading: loadingStations, error: props.stationError, ascii: ascii, reduceMotion: Boolean(library.settings.reduceMotion) }));
|
|
31
40
|
}
|
|
32
41
|
if (screen === 'search') {
|
|
33
42
|
const rows = adaptiveRows({
|
|
34
43
|
screen,
|
|
44
|
+
settingsPage,
|
|
35
45
|
stations,
|
|
36
46
|
countries,
|
|
37
47
|
airPlayDevices,
|
|
@@ -39,6 +49,7 @@ function AdaptiveContentBody(props) {
|
|
|
39
49
|
diagnostics,
|
|
40
50
|
backends,
|
|
41
51
|
updateCheck,
|
|
52
|
+
appVersion,
|
|
42
53
|
favoriteKeys,
|
|
43
54
|
selected,
|
|
44
55
|
width,
|
|
@@ -53,9 +64,10 @@ function AdaptiveContentBody(props) {
|
|
|
53
64
|
loadingCountries,
|
|
54
65
|
loadingStations,
|
|
55
66
|
nearbyEnabled: library.settings.enableNearbyLocation,
|
|
56
|
-
stationTitle
|
|
67
|
+
stationTitle,
|
|
68
|
+
stationError: props.stationError
|
|
57
69
|
});
|
|
58
|
-
return (_jsx(AdaptiveSearch, { mode: mode, query: searchQuery, editing: editingSearch, loading: loadingStations, rows: rows, empty: empty, selected: selected, height: height, width: width, theme: theme, ascii: ascii }));
|
|
70
|
+
return (_jsx(AdaptiveSearch, { mode: mode, query: searchQuery, editing: editingSearch, loading: loadingStations, rows: rows, empty: empty, selected: selected, height: height, width: width, theme: theme, ascii: ascii, reduceMotion: Boolean(library.settings.reduceMotion) }));
|
|
59
71
|
}
|
|
60
72
|
if (screen === 'stats') {
|
|
61
73
|
const stats = computeListeningStats(library.activity.sessions);
|
|
@@ -78,6 +90,7 @@ function AdaptiveContentBody(props) {
|
|
|
78
90
|
}
|
|
79
91
|
const rows = adaptiveRows({
|
|
80
92
|
screen,
|
|
93
|
+
settingsPage,
|
|
81
94
|
stations,
|
|
82
95
|
countries,
|
|
83
96
|
airPlayDevices,
|
|
@@ -85,6 +98,7 @@ function AdaptiveContentBody(props) {
|
|
|
85
98
|
diagnostics,
|
|
86
99
|
backends,
|
|
87
100
|
updateCheck,
|
|
101
|
+
appVersion,
|
|
88
102
|
favoriteKeys,
|
|
89
103
|
selected,
|
|
90
104
|
width,
|
|
@@ -99,9 +113,55 @@ function AdaptiveContentBody(props) {
|
|
|
99
113
|
loadingCountries,
|
|
100
114
|
loadingStations,
|
|
101
115
|
nearbyEnabled: library.settings.enableNearbyLocation,
|
|
102
|
-
stationTitle
|
|
116
|
+
stationTitle,
|
|
117
|
+
stationError: props.stationError
|
|
103
118
|
});
|
|
104
|
-
return (_jsx(AdaptiveFrame, { title: title, status: status, mode: mode, height: height, width: width, theme: theme, children: rows.length > 0 ? (_jsx(AdaptiveList, { rows: rows, selected: selected, pageSize: bodyRows, width: width, theme: theme })) : (_jsx(StaticRows, { rows: empty.slice(0, bodyRows), width: width, theme: theme })) }));
|
|
119
|
+
return (_jsx(AdaptiveFrame, { title: title, status: status, mode: mode, height: height, width: width, theme: theme, children: rows.length > 0 ? (_jsx(AdaptiveList, { rows: rows, selected: selected, pageSize: bodyRows, width: width, theme: theme, reduceMotion: Boolean(library.settings.reduceMotion) })) : (_jsx(StaticRows, { rows: empty.slice(0, bodyRows), width: width, theme: theme })) }));
|
|
120
|
+
}
|
|
121
|
+
function AdaptiveExplore({ mode, width, height, theme, stations, selected, favorites, cursor, loading, error, ascii, reduceMotion }) {
|
|
122
|
+
const accent = themeAccent(theme);
|
|
123
|
+
const { headerRows, headerGap, bodyRows } = adaptiveExploreFrameMetrics(mode, height);
|
|
124
|
+
const layout = computeAdaptiveExploreLayout(mode, width, Math.max(1, bodyRows));
|
|
125
|
+
const marker = React.useMemo(() => [{ lat: cursor.latitude, lon: cursor.longitude, selected: true }], [cursor.latitude, cursor.longitude]);
|
|
126
|
+
const map = React.useMemo(() => buildCosmoWorldMap(layout.mapColumns, layout.mapRows, marker), [layout.mapColumns, layout.mapRows, marker]);
|
|
127
|
+
const coordinate = formatExploreCursor(cursor);
|
|
128
|
+
const status = `${coordinate} · ${loading ? 'loading' : `${stations.length.toLocaleString()} stations`}`;
|
|
129
|
+
const title = mode === 'micro' ? `RC / Explore · ${coordinate}` : 'RADIOCLI Explore';
|
|
130
|
+
const stationRows = stationAdaptiveRows(stations, favorites, selected, layout.listWidth, ascii);
|
|
131
|
+
const emptyRows = loading
|
|
132
|
+
? [{ key: 'loading', label: 'Loading stations…' }]
|
|
133
|
+
: error
|
|
134
|
+
? [{ key: 'error', label: error }]
|
|
135
|
+
: [{ key: 'empty', label: 'No stations near this point.' }];
|
|
136
|
+
const list = (_jsx(AdaptiveList, { rows: stationRows.length > 0 ? stationRows : emptyRows, selected: stationRows.length > 0 ? selected : 0, pageSize: layout.listRows, width: layout.listWidth, theme: theme, reduceMotion: reduceMotion }));
|
|
137
|
+
return (_jsxs(Box, { flexDirection: "column", height: height, width: width, overflow: "hidden", children: [_jsx(Text, { color: accent, bold: true, children: truncate(title, width) }), headerRows > 1 ? _jsx(Text, { color: textMuted, children: truncate(status, width) }) : null, headerGap ? _jsx(Box, { height: headerGap, flexShrink: 0 }) : null, bodyRows > 0 ? layout.split ? (_jsxs(Box, { flexDirection: "row", height: bodyRows, width: width, overflow: "hidden", children: [_jsx(AdaptiveCosmoMap, { rows: map, width: layout.mapAreaWidth, height: layout.mapRows, offsetX: layout.mapOffsetX, theme: theme, ascii: ascii }), layout.gap ? _jsx(Box, { width: layout.gap, flexShrink: 0 }) : null, _jsx(Box, { flexDirection: "column", width: layout.listWidth, height: layout.listRows, overflow: "hidden", children: list })] })) : (_jsxs(Box, { flexDirection: "column", height: bodyRows, width: width, overflow: "hidden", children: [_jsx(AdaptiveCosmoMap, { rows: map, width: layout.mapAreaWidth, height: layout.mapRows, offsetX: layout.mapOffsetX, theme: theme, ascii: ascii }), layout.gap ? _jsx(Box, { height: layout.gap, flexShrink: 0 }) : null, _jsx(Box, { flexDirection: "column", height: layout.listRows, width: layout.listWidth, overflow: "hidden", children: list })] })) : null] }));
|
|
138
|
+
}
|
|
139
|
+
function AdaptiveCosmoMap({ rows, width, height, offsetX, theme, ascii }) {
|
|
140
|
+
return (_jsx(Box, { flexDirection: "column", width: width, height: height, overflow: "hidden", flexShrink: 0, children: rows.map((row, rowIndex) => {
|
|
141
|
+
const chunks = [];
|
|
142
|
+
for (const cell of row.cells) {
|
|
143
|
+
const previous = chunks.at(-1);
|
|
144
|
+
if (previous?.kind === cell.kind)
|
|
145
|
+
previous.text += cell.char;
|
|
146
|
+
else
|
|
147
|
+
chunks.push({ kind: cell.kind, text: cell.char });
|
|
148
|
+
}
|
|
149
|
+
let cellOffset = 0;
|
|
150
|
+
return (_jsx(Box, { marginLeft: offsetX, children: chunks.map(chunk => {
|
|
151
|
+
const key = `${cellOffset}-${chunk.kind}`;
|
|
152
|
+
cellOffset += chunk.text.length;
|
|
153
|
+
return (_jsx(Text, { color: adaptiveMapColor(chunk.kind, theme), children: ascii ? toAsciiSafe(chunk.text) : chunk.text }, key));
|
|
154
|
+
}) }, rowIndex));
|
|
155
|
+
}) }));
|
|
156
|
+
}
|
|
157
|
+
function adaptiveMapColor(kind, theme) {
|
|
158
|
+
if (kind === 'selected')
|
|
159
|
+
return themeAccent(theme);
|
|
160
|
+
if (kind === 'marker')
|
|
161
|
+
return mapMarker;
|
|
162
|
+
if (kind === 'land')
|
|
163
|
+
return exploreMapLand;
|
|
164
|
+
return undefined;
|
|
105
165
|
}
|
|
106
166
|
function AdaptiveFrame({ title, status, mode, height, width, theme, children }) {
|
|
107
167
|
const accent = themeAccent(theme);
|
|
@@ -134,7 +194,7 @@ function AdaptiveHome({ mode, selected, height, width, theme, library }) {
|
|
|
134
194
|
return (_jsxs(Text, { color: active ? accent : undefined, bold: active, children: [active ? '> ' : ' ', absoluteIndex + 1, " ", truncate(`${item.label}${detail}`, Math.max(1, width - 4))] }, item.screen));
|
|
135
195
|
}) }), showSummary ? (_jsx(Text, { color: textMuted, children: truncate(`${library.recent.length} recent · ${library.favorites.length} favorites · ${library.imported.length} imported`, width) })) : null, gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null] }));
|
|
136
196
|
}
|
|
137
|
-
function AdaptiveList({ rows, selected, pageSize, width, theme }) {
|
|
197
|
+
function AdaptiveList({ rows, selected, pageSize, width, theme, reduceMotion }) {
|
|
138
198
|
const selectedIndex = Math.min(Math.max(selected, 0), Math.max(0, rows.length - 1));
|
|
139
199
|
const window = visibleWindow(rows, selectedIndex, Math.max(1, pageSize));
|
|
140
200
|
const accent = themeAccent(theme);
|
|
@@ -143,14 +203,17 @@ function AdaptiveList({ rows, selected, pageSize, width, theme }) {
|
|
|
143
203
|
const active = absoluteIndex === selectedIndex && !row.heading;
|
|
144
204
|
const prefix = row.heading ? ' ' : active ? '> ' : ' ';
|
|
145
205
|
const detail = row.detail ? `${row.separator ?? ' · '}${row.detail}` : '';
|
|
146
|
-
|
|
206
|
+
const text = `${row.label}${detail}`;
|
|
207
|
+
const favoriteWidth = row.favoriteGlyph ? 2 : 0;
|
|
208
|
+
return (_jsxs(Text, { color: active ? accent : row.heading ? textMuted : undefined, bold: active || row.heading, children: [prefix, _jsx(AdaptiveMarquee, { text: text, width: Math.max(0, width - 2 - favoriteWidth), active: active && Boolean(row.marquee), reduceMotion: reduceMotion }), row.favoriteGlyph ? _jsxs(Text, { color: "yellow", children: [" ", row.favoriteGlyph] }) : null] }, row.key));
|
|
147
209
|
}) }));
|
|
148
210
|
}
|
|
149
211
|
function StaticRows({ rows, width, theme }) {
|
|
150
212
|
const accent = themeAccent(theme);
|
|
151
213
|
return (_jsx(Box, { flexDirection: "column", children: rows.map(row => (_jsxs(Text, { color: row.heading ? textMuted : undefined, bold: row.heading, children: [truncate(row.detail ? `${row.label} · ${row.detail}` : row.label, width), row.key === 'activity' ? _jsx(Text, { color: accent }) : null] }, row.key))) }));
|
|
152
214
|
}
|
|
153
|
-
function AdaptiveNowPlaying({ mode, station, playback, metadata,
|
|
215
|
+
function AdaptiveNowPlaying({ mode, station, playback, metadata, width, height, ascii, theme, receiverStyle, reduceMotion }) {
|
|
216
|
+
const pulse = useReceiverPulse();
|
|
154
217
|
const accent = themeAccent(theme);
|
|
155
218
|
const stationName = station?.name ?? 'No station tuned';
|
|
156
219
|
const showMetadata = mode === 'compact' && height >= 9 && Boolean(metadata?.title);
|
|
@@ -164,21 +227,23 @@ function AdaptiveNowPlaying({ mode, station, playback, metadata, pulse, width, h
|
|
|
164
227
|
? `${stationName} · ${playback.state}`
|
|
165
228
|
: 'RADIOCLI Now playing';
|
|
166
229
|
const status = `${stationName} ${playback.state}${playback.muted ? ' · muted' : ` · vol ${playback.volume}`}`;
|
|
167
|
-
return (_jsxs(Box, { flexDirection: "column", height: height, width: width, overflow: "hidden", children: [_jsx(Text, { color: accent, bold: true, children:
|
|
230
|
+
return (_jsxs(Box, { flexDirection: "column", height: height, width: width, overflow: "hidden", children: [_jsx(Text, { color: accent, bold: true, children: mode === 'micro'
|
|
231
|
+
? _jsx(AdaptiveMarquee, { text: header, width: width, active: true, reduceMotion: reduceMotion })
|
|
232
|
+
: truncate(header, width) }), mode === 'compact' ? _jsx(Text, { color: textMuted, children: truncate(status, width) }) : null, gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null, _jsx(Box, { flexDirection: "column", height: availableVisualRows, overflow: "hidden", children: visualRows.map((row, index) => (_jsx(Text, { color: row.segments ? undefined : row.color, children: row.segments
|
|
168
233
|
? renderAdaptiveSegments(row.segments, ascii)
|
|
169
234
|
: ascii
|
|
170
235
|
? toAsciiSafe(row.text)
|
|
171
|
-
: row.text }, index))) }), showMetadata ? _jsx(Text, { color: accent, children:
|
|
236
|
+
: row.text }, index))) }), showMetadata ? (_jsx(Text, { color: accent, children: _jsx(AdaptiveMarquee, { text: metadata?.title ?? '', width: width, active: true, reduceMotion: reduceMotion }) })) : null, gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null] }));
|
|
172
237
|
}
|
|
173
238
|
function renderAdaptiveSegments(segments, ascii) {
|
|
174
239
|
let offset = 0;
|
|
175
240
|
return segments.map(segment => {
|
|
176
|
-
const key =
|
|
241
|
+
const key = offset;
|
|
177
242
|
offset += segment.text.length;
|
|
178
243
|
return (_jsx(Text, { color: segment.color, backgroundColor: segment.backgroundColor, bold: segment.bold, children: ascii ? toAsciiSafe(segment.text) : segment.text }, key));
|
|
179
244
|
});
|
|
180
245
|
}
|
|
181
|
-
function AdaptiveSearch({ mode, query, editing, loading, rows, empty, selected, height, width, theme, ascii }) {
|
|
246
|
+
function AdaptiveSearch({ mode, query, editing, loading, rows, empty, selected, height, width, theme, ascii, reduceMotion }) {
|
|
182
247
|
const accent = themeAccent(theme);
|
|
183
248
|
const { panel: panelBackground } = useDisplay();
|
|
184
249
|
const fieldText = query || 'station, genre, or place';
|
|
@@ -186,7 +251,7 @@ function AdaptiveSearch({ mode, query, editing, loading, rows, empty, selected,
|
|
|
186
251
|
const fieldRows = height >= 4 ? 3 : 1;
|
|
187
252
|
const gapRows = height >= 6 ? 1 : 0;
|
|
188
253
|
const listRows = Math.max(0, height - 1 - fieldRows - gapRows * 2);
|
|
189
|
-
return (_jsxs(Box, { flexDirection: "column", height: height, width: width, overflow: "hidden", children: [_jsx(Text, { color: accent, bold: true, children: mode === 'micro' ? 'SEARCH' : 'RADIOCLI Search' }), fieldRows === 3 ? (_jsxs(Box, { borderStyle: panelBorderStyle(ascii, 'single'), borderColor: editing || loading ? accent : panelBorder, borderBackgroundColor: panelBackground, backgroundColor: panelBackground, width: width, height: 3, flexShrink: 0, children: [_jsx(Text, { color: editing || loading ? accent : textMuted, children: prefix }), _jsx(Text, { color: query ? accent : textMuted, children: truncate(fieldText, Math.max(1, width - 5)) })] })) : (_jsx(Text, { color: query ? accent : textMuted, children: truncate(`[${prefix}${fieldText}]`, width) })), gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null, _jsx(Box, { flexDirection: "column", height: listRows, overflow: "hidden", flexShrink: 0, children: rows.length > 0 ? (_jsx(AdaptiveList, { rows: rows, selected: selected, pageSize: listRows, width: width, theme: theme })) : (_jsx(StaticRows, { rows: empty.slice(0, listRows), width: width, theme: theme })) }), gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null] }));
|
|
254
|
+
return (_jsxs(Box, { flexDirection: "column", height: height, width: width, overflow: "hidden", children: [_jsx(Text, { color: accent, bold: true, children: mode === 'micro' ? 'SEARCH' : 'RADIOCLI Search' }), fieldRows === 3 ? (_jsxs(Box, { borderStyle: panelBorderStyle(ascii, 'single'), borderColor: editing || loading ? accent : panelBorder, borderBackgroundColor: panelBackground, backgroundColor: panelBackground, width: width, height: 3, flexShrink: 0, children: [_jsx(Text, { color: editing || loading ? accent : textMuted, children: prefix }), _jsx(Text, { color: query ? accent : textMuted, children: truncate(fieldText, Math.max(1, width - 5)) })] })) : (_jsx(Text, { color: query ? accent : textMuted, children: truncate(`[${prefix}${fieldText}]`, width) })), gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null, _jsx(Box, { flexDirection: "column", height: listRows, overflow: "hidden", flexShrink: 0, children: rows.length > 0 ? (_jsx(AdaptiveList, { rows: rows, selected: selected, pageSize: listRows, width: width, theme: theme, reduceMotion: reduceMotion })) : (_jsx(StaticRows, { rows: empty.slice(0, listRows), width: width, theme: theme })) }), gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null] }));
|
|
190
255
|
}
|
|
191
256
|
function adaptiveRows(input) {
|
|
192
257
|
const { screen, stations, countries, airPlayDevices, library, diagnostics, backends, updateCheck, favoriteKeys, selected, width, mode, ascii } = input;
|
|
@@ -194,16 +259,20 @@ function adaptiveRows(input) {
|
|
|
194
259
|
return homeItems.map(item => ({
|
|
195
260
|
key: item.screen,
|
|
196
261
|
label: item.label,
|
|
197
|
-
detail: mode === 'compact' && width >= 52 ? item.detail : undefined
|
|
262
|
+
detail: mode === 'compact' && width >= 52 ? item.detail : undefined,
|
|
263
|
+
separator: ' '
|
|
198
264
|
}));
|
|
199
265
|
}
|
|
200
266
|
if (screen === 'settings') {
|
|
201
|
-
const
|
|
267
|
+
const pageItems = settingsItemsForPage(input.settingsPage);
|
|
268
|
+
const labels = pageItems.map(item => settingLabel(item, updateCheck, input.appVersion));
|
|
202
269
|
const labelWidth = pairedColumnWidth(labels, width, mode);
|
|
203
|
-
return
|
|
270
|
+
return pageItems.map((item, index) => ({
|
|
204
271
|
key: item,
|
|
205
|
-
label: padDisplayEnd(truncate(settingLabel(item, updateCheck), labelWidth), labelWidth),
|
|
206
|
-
detail:
|
|
272
|
+
label: padDisplayEnd(truncate(settingLabel(item, updateCheck, input.appVersion), labelWidth), labelWidth),
|
|
273
|
+
detail: input.settingsPage === 'root'
|
|
274
|
+
? adaptiveSettingsRootValue(item)
|
|
275
|
+
: settingValue(item, library.settings, diagnostics, backends, airPlayDevices, updateCheck, input.appVersion),
|
|
207
276
|
separator: ' ',
|
|
208
277
|
index
|
|
209
278
|
}));
|
|
@@ -216,6 +285,7 @@ function adaptiveRows(input) {
|
|
|
216
285
|
label: padDisplayEnd(truncate(`${country.name} (${country.code})`, labelWidth), labelWidth),
|
|
217
286
|
detail: country.stationCount.toLocaleString(),
|
|
218
287
|
separator: ' ',
|
|
288
|
+
marquee: true,
|
|
219
289
|
index
|
|
220
290
|
}));
|
|
221
291
|
}
|
|
@@ -239,14 +309,23 @@ function adaptiveRows(input) {
|
|
|
239
309
|
}
|
|
240
310
|
return rows;
|
|
241
311
|
}
|
|
312
|
+
return stationAdaptiveRows(stations, favoriteKeys, selected, width, ascii);
|
|
313
|
+
}
|
|
314
|
+
function adaptiveSettingsRootValue(item) {
|
|
315
|
+
const group = settingsGroups.find(candidate => candidate.label === item);
|
|
316
|
+
return group ? `${group.items.length} settings ›` : undefined;
|
|
317
|
+
}
|
|
318
|
+
function stationAdaptiveRows(stations, favoriteKeys, selected, width, ascii) {
|
|
242
319
|
return stations.map((station, index) => {
|
|
243
320
|
const favorite = favoriteKeys.has(`${station.provider}:${station.id}`);
|
|
244
321
|
const standardMetadata = `${stationLocation(station)} · ${stationTech(station)}`;
|
|
245
322
|
const selectedMetadata = station.tags.length > 0 ? stationTags(station) : standardMetadata;
|
|
246
323
|
return {
|
|
247
324
|
key: `${station.provider}:${station.id}`,
|
|
248
|
-
label:
|
|
249
|
-
detail:
|
|
325
|
+
label: station.name,
|
|
326
|
+
detail: index === selected ? selectedMetadata : width >= 42 ? standardMetadata : undefined,
|
|
327
|
+
marquee: true,
|
|
328
|
+
favoriteGlyph: favorite ? (ascii ? '*' : '★') : undefined,
|
|
250
329
|
index
|
|
251
330
|
};
|
|
252
331
|
});
|
|
@@ -258,7 +337,7 @@ function pairedColumnWidth(labels, width, mode) {
|
|
|
258
337
|
return Math.min(longest, cap);
|
|
259
338
|
}
|
|
260
339
|
function adaptiveEmptyState(input) {
|
|
261
|
-
const { screen, searchQuery, editingSearch, countryFilter, loadingCountries, loadingStations, nearbyEnabled, stationTitle } = input;
|
|
340
|
+
const { screen, searchQuery, editingSearch, countryFilter, loadingCountries, loadingStations, nearbyEnabled, stationTitle, stationError } = input;
|
|
262
341
|
if ((screen === 'countries' || screen === 'map') && loadingCountries) {
|
|
263
342
|
return [{ key: 'loading', label: 'Loading countries…' }];
|
|
264
343
|
}
|
|
@@ -286,13 +365,16 @@ function adaptiveEmptyState(input) {
|
|
|
286
365
|
if (loadingStations) {
|
|
287
366
|
return [{ key: 'loading', label: 'Loading stations…' }];
|
|
288
367
|
}
|
|
368
|
+
if (stationError) {
|
|
369
|
+
return [{ key: 'error', label: stationError }, { key: 'hint', label: 'Try again later or reopen this view.' }];
|
|
370
|
+
}
|
|
289
371
|
if (screen === 'airplay-settings') {
|
|
290
372
|
return [{ key: 'empty', label: 'No AirPlay receivers found.' }, { key: 'hint', label: 'Press r to scan again.' }];
|
|
291
373
|
}
|
|
292
374
|
return [{ key: 'empty', label: `No stations in ${stationTitle.toLowerCase()}.` }, { key: 'hint', label: 'Try another view or clear filters.' }];
|
|
293
375
|
}
|
|
294
376
|
function adaptiveStatus(props) {
|
|
295
|
-
const { screen, playback, playingStation, stations, countries, searchQuery, editingSearch, countryFilter, editingCountryFilter, library, filterLabel
|
|
377
|
+
const { screen, playback, playingStation, stations, countries, searchQuery, editingSearch, countryFilter, editingCountryFilter, library, filterLabel } = props;
|
|
296
378
|
if (screen === 'home') {
|
|
297
379
|
return `${library.favorites.length} favorites · ${library.recent.length} recent · ${library.imported.length} imported`;
|
|
298
380
|
}
|
|
@@ -300,7 +382,10 @@ function adaptiveStatus(props) {
|
|
|
300
382
|
return `${playbackBackendLabel(playback.backend)} · ${playback.state} · vol ${playback.volume}`;
|
|
301
383
|
}
|
|
302
384
|
if (screen === 'settings') {
|
|
303
|
-
|
|
385
|
+
const group = settingsGroup(props.settingsPage ?? 'root');
|
|
386
|
+
return group
|
|
387
|
+
? `${group.label} · Enter changes setting · b categories`
|
|
388
|
+
: 'Choose a category · Updates are available here';
|
|
304
389
|
}
|
|
305
390
|
if (screen === 'search') {
|
|
306
391
|
const query = searchQuery || (editingSearch ? 'type to search' : 'press / to search');
|