@ciphore/radiocli 0.2.2 → 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 +124 -1
- package/CONTRIBUTING.md +36 -6
- package/README.md +84 -10
- package/dist/agent/alarm-service.js +210 -0
- package/dist/agent/cli.js +193 -0
- package/dist/agent/headless-host.js +165 -0
- package/dist/agent/launcher.js +10 -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 +232 -0
- package/dist/alarms/active-session.js +9 -12
- package/dist/alarms/cli.js +4 -1
- 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 +96 -48
- 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 +157 -35
- 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 +220 -31
- package/dist/providers/cache.js +4 -26
- package/dist/providers/provider-manager.js +5 -0
- package/dist/providers/radio-browser.js +156 -54
- package/dist/providers/radio-garden.js +15 -22
- package/dist/setup.js +149 -152
- package/dist/storage/store.js +138 -53
- package/dist/streams/import-stream.js +163 -0
- package/dist/types.js +6 -0
- package/dist/ui/AdaptiveContent.js +54 -30
- package/dist/ui/App.js +466 -98
- package/dist/ui/AppContent.js +4 -4
- 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 +8 -8
- package/dist/ui/components/TopTabs.js +1 -1
- package/dist/ui/components/VersionIndicator.js +19 -0
- 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 +5 -3
- package/dist/ui/screen-items.js +42 -11
- 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/AlarmsScreen.js +2 -1
- package/dist/ui/screens/CountriesScreen.js +8 -5
- package/dist/ui/screens/ExploreScreen.js +2 -1
- package/dist/ui/screens/HelpScreen.js +5 -1
- package/dist/ui/screens/HomeScreen.js +7 -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 +80 -56
- 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 +40 -21
- package/dist/ui/use-app-input.js +69 -23
- package/dist/ui/use-command-executor.js +31 -7
- 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 +33 -23
- package/package.json +4 -1
|
@@ -5,7 +5,7 @@ import { computeListeningStats } from '../activity/stats.js';
|
|
|
5
5
|
import { playbackBackendLabel } from '../player/backend-install.js';
|
|
6
6
|
import { visibleWindow } from './list-window.js';
|
|
7
7
|
import { displayWidth, padDisplayEnd, stationLocation, stationTags, stationTech, truncate } from './format.js';
|
|
8
|
-
import { homeItems,
|
|
8
|
+
import { homeItems, settingsGroup, settingsGroups, settingsItemsForPage } from './screen-items.js';
|
|
9
9
|
import { screenTitle } from './screen-meta.js';
|
|
10
10
|
import { keyHelpSections, commandHelp } from './help-content.js';
|
|
11
11
|
import { settingLabel, settingValue } from './screens/SettingsScreen.js';
|
|
@@ -23,9 +23,9 @@ export function AdaptiveContent(props) {
|
|
|
23
23
|
return _jsx(AdaptiveContentBody, { ...props });
|
|
24
24
|
}
|
|
25
25
|
function AdaptiveContentBody(props) {
|
|
26
|
-
const { mode, screen, selected, height, width, theme, playback, playingStation, nowPlaying, stations, countries, airPlayDevices, airPlayCode, searchQuery, editingSearch, countryFilter, loadingCountries, loadingStations, exploreCursor, library, diagnostics, backends, updateCheck, favoriteKeys, stationTitle, } = props;
|
|
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, airPlaySupported = true, } = props;
|
|
27
27
|
const accent = themeAccent(theme);
|
|
28
|
-
const { ascii } = useDisplay();
|
|
28
|
+
const { ascii, reduceMotion } = useDisplay();
|
|
29
29
|
const { bodyRows } = adaptiveFrameMetrics(mode, height);
|
|
30
30
|
const title = screenTitle(screen);
|
|
31
31
|
const status = adaptiveStatus(props);
|
|
@@ -33,14 +33,15 @@ function AdaptiveContentBody(props) {
|
|
|
33
33
|
return (_jsx(AdaptiveHome, { mode: mode, selected: selected, height: height, width: width, theme: theme, library: library }));
|
|
34
34
|
}
|
|
35
35
|
if (screen === 'now-playing') {
|
|
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:
|
|
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: reduceMotion }));
|
|
37
37
|
}
|
|
38
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:
|
|
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: reduceMotion }));
|
|
40
40
|
}
|
|
41
41
|
if (screen === 'search') {
|
|
42
42
|
const rows = adaptiveRows({
|
|
43
43
|
screen,
|
|
44
|
+
settingsPage,
|
|
44
45
|
stations,
|
|
45
46
|
countries,
|
|
46
47
|
airPlayDevices,
|
|
@@ -48,11 +49,13 @@ function AdaptiveContentBody(props) {
|
|
|
48
49
|
diagnostics,
|
|
49
50
|
backends,
|
|
50
51
|
updateCheck,
|
|
52
|
+
appVersion,
|
|
51
53
|
favoriteKeys,
|
|
52
54
|
selected,
|
|
53
55
|
width,
|
|
54
56
|
mode,
|
|
55
|
-
ascii
|
|
57
|
+
ascii,
|
|
58
|
+
airPlaySupported
|
|
56
59
|
});
|
|
57
60
|
const empty = adaptiveEmptyState({
|
|
58
61
|
screen,
|
|
@@ -65,7 +68,7 @@ function AdaptiveContentBody(props) {
|
|
|
65
68
|
stationTitle,
|
|
66
69
|
stationError: props.stationError
|
|
67
70
|
});
|
|
68
|
-
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:
|
|
71
|
+
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: reduceMotion }));
|
|
69
72
|
}
|
|
70
73
|
if (screen === 'stats') {
|
|
71
74
|
const stats = computeListeningStats(library.activity.sessions);
|
|
@@ -84,10 +87,11 @@ function AdaptiveContentBody(props) {
|
|
|
84
87
|
}
|
|
85
88
|
if (screen === 'airplay-code') {
|
|
86
89
|
const prompt = airPlayCode ? `Code: ${airPlayCode}` : 'Type the code shown by the receiver.';
|
|
87
|
-
return (_jsx(AdaptiveFrame, { title: title, status: status, mode: mode, height: height, width: width, theme: theme, children: bodyRows > 0 ? _jsx(Text, { color: airPlayCode ? accent : textMuted, children: truncate(prompt, width) }) : null }));
|
|
90
|
+
return (_jsx(AdaptiveFrame, { title: title, status: status, mode: mode, height: height, width: width, theme: theme, children: bodyRows > 0 ? _jsx(Text, { color: airPlayCode ? accent : textMuted, children: ascii ? toAsciiSafe(truncate(prompt, width)) : truncate(prompt, width) }) : null }));
|
|
88
91
|
}
|
|
89
92
|
const rows = adaptiveRows({
|
|
90
93
|
screen,
|
|
94
|
+
settingsPage,
|
|
91
95
|
stations,
|
|
92
96
|
countries,
|
|
93
97
|
airPlayDevices,
|
|
@@ -95,11 +99,13 @@ function AdaptiveContentBody(props) {
|
|
|
95
99
|
diagnostics,
|
|
96
100
|
backends,
|
|
97
101
|
updateCheck,
|
|
102
|
+
appVersion,
|
|
98
103
|
favoriteKeys,
|
|
99
104
|
selected,
|
|
100
105
|
width,
|
|
101
106
|
mode,
|
|
102
|
-
ascii
|
|
107
|
+
ascii,
|
|
108
|
+
airPlaySupported
|
|
103
109
|
});
|
|
104
110
|
const empty = adaptiveEmptyState({
|
|
105
111
|
screen,
|
|
@@ -112,11 +118,12 @@ function AdaptiveContentBody(props) {
|
|
|
112
118
|
stationTitle,
|
|
113
119
|
stationError: props.stationError
|
|
114
120
|
});
|
|
115
|
-
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:
|
|
121
|
+
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: reduceMotion })) : (_jsx(StaticRows, { rows: empty.slice(0, bodyRows), width: width, theme: theme })) }));
|
|
116
122
|
}
|
|
117
123
|
function AdaptiveExplore({ mode, width, height, theme, stations, selected, favorites, cursor, loading, error, ascii, reduceMotion }) {
|
|
118
124
|
const accent = themeAccent(theme);
|
|
119
125
|
const { headerRows, headerGap, bodyRows } = adaptiveExploreFrameMetrics(mode, height);
|
|
126
|
+
const a = (value) => ascii ? toAsciiSafe(value) : value;
|
|
120
127
|
const layout = computeAdaptiveExploreLayout(mode, width, Math.max(1, bodyRows));
|
|
121
128
|
const marker = React.useMemo(() => [{ lat: cursor.latitude, lon: cursor.longitude, selected: true }], [cursor.latitude, cursor.longitude]);
|
|
122
129
|
const map = React.useMemo(() => buildCosmoWorldMap(layout.mapColumns, layout.mapRows, marker), [layout.mapColumns, layout.mapRows, marker]);
|
|
@@ -130,10 +137,10 @@ function AdaptiveExplore({ mode, width, height, theme, stations, selected, favor
|
|
|
130
137
|
? [{ key: 'error', label: error }]
|
|
131
138
|
: [{ key: 'empty', label: 'No stations near this point.' }];
|
|
132
139
|
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 }));
|
|
133
|
-
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] }));
|
|
140
|
+
return (_jsxs(Box, { flexDirection: "column", height: height, width: width, overflow: "hidden", children: [_jsx(Text, { color: accent, bold: true, "aria-label": title, children: a(truncate(title, width)) }), headerRows > 1 ? _jsx(Text, { color: textMuted, "aria-label": status, children: a(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] }));
|
|
134
141
|
}
|
|
135
142
|
function AdaptiveCosmoMap({ rows, width, height, offsetX, theme, ascii }) {
|
|
136
|
-
return (_jsx(Box, { flexDirection: "column", width: width, height: height, overflow: "hidden", flexShrink: 0, children: rows.map((row, rowIndex) => {
|
|
143
|
+
return (_jsx(Box, { flexDirection: "column", width: width, height: height, overflow: "hidden", flexShrink: 0, "aria-hidden": true, children: rows.map((row, rowIndex) => {
|
|
137
144
|
const chunks = [];
|
|
138
145
|
for (const cell of row.cells) {
|
|
139
146
|
const previous = chunks.at(-1);
|
|
@@ -162,11 +169,12 @@ function adaptiveMapColor(kind, theme) {
|
|
|
162
169
|
function AdaptiveFrame({ title, status, mode, height, width, theme, children }) {
|
|
163
170
|
const accent = themeAccent(theme);
|
|
164
171
|
const { ascii } = useDisplay();
|
|
172
|
+
const a = (value) => ascii ? toAsciiSafe(value) : value;
|
|
165
173
|
const titlePrefix = mode === 'micro' ? 'RC / ' : 'RADIOCLI ';
|
|
166
174
|
const titleText = truncate(`${titlePrefix}${title}${mode === 'micro' && status ? ` · ${status}` : ''}`, width);
|
|
167
175
|
const ruleWidth = Math.max(0, width - displayWidth(titleText) - 1);
|
|
168
176
|
const { bodyRows, gapRows } = adaptiveFrameMetrics(mode, height);
|
|
169
|
-
return (_jsxs(Box, { flexDirection: "column", height: height, width: width, overflow: "hidden", children: [_jsxs(Text, { color: accent, bold: true, children: [titleText, _jsx(Text, { color: textDim, children: ruleWidth ? ` ${(ascii ? '-' : '─').repeat(ruleWidth)}` : '' })] }), mode === 'compact' && height >= 4 ? _jsx(Text, { color: textMuted, children: truncate(status, width) }) : null, gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null, _jsx(Box, { flexDirection: "column", height: bodyRows, overflow: "hidden", flexShrink: 0, children: children }), gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null] }));
|
|
177
|
+
return (_jsxs(Box, { flexDirection: "column", height: height, width: width, overflow: "hidden", children: [_jsxs(Text, { color: accent, bold: true, "aria-label": `${titlePrefix}${title}${status ? `. ${status}` : ''}`, children: [a(titleText), _jsx(Text, { color: textDim, "aria-hidden": true, children: ruleWidth ? ` ${(ascii ? '-' : '─').repeat(ruleWidth)}` : '' })] }), mode === 'compact' && height >= 4 ? _jsx(Text, { color: textMuted, "aria-label": status, children: a(truncate(status, width)) }) : null, gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null, _jsx(Box, { flexDirection: "column", height: bodyRows, overflow: "hidden", flexShrink: 0, children: children }), gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null] }));
|
|
170
178
|
}
|
|
171
179
|
function adaptiveFrameMetrics(mode, height) {
|
|
172
180
|
const headerRows = mode === 'compact' && height >= 4 ? 2 : 1;
|
|
@@ -178,6 +186,8 @@ function adaptiveFrameMetrics(mode, height) {
|
|
|
178
186
|
}
|
|
179
187
|
function AdaptiveHome({ mode, selected, height, width, theme, library }) {
|
|
180
188
|
const gapRows = height >= 5 ? 1 : 0;
|
|
189
|
+
const { ascii } = useDisplay();
|
|
190
|
+
const a = (value) => ascii ? toAsciiSafe(value) : value;
|
|
181
191
|
const showSummary = mode === 'compact' && height >= 10;
|
|
182
192
|
const menuRows = Math.max(1, height - 1 - gapRows * 2 - (showSummary ? 1 : 0));
|
|
183
193
|
const selectedIndex = Math.min(Math.max(selected, 0), homeItems.length - 1);
|
|
@@ -187,8 +197,8 @@ function AdaptiveHome({ mode, selected, height, width, theme, library }) {
|
|
|
187
197
|
const absoluteIndex = window.start + offset;
|
|
188
198
|
const active = absoluteIndex === selectedIndex;
|
|
189
199
|
const detail = mode === 'compact' && width >= 52 ? ` · ${item.detail}` : '';
|
|
190
|
-
return (_jsxs(Text, { color: active ? accent : undefined, bold: active, children: [active ? '> ' : ' ', absoluteIndex + 1, " ", truncate(`${item.label}${detail}`, Math.max(1, width - 4))] }, item.screen));
|
|
191
|
-
}) }), 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] }));
|
|
200
|
+
return (_jsxs(Text, { color: active ? accent : undefined, bold: active, "aria-label": `${active ? 'Selected: ' : ''}${absoluteIndex + 1}. ${item.label}. ${item.detail}`, children: [active ? '> ' : ' ', absoluteIndex + 1, " ", a(truncate(`${item.label}${detail}`, Math.max(1, width - 4)))] }, item.screen));
|
|
201
|
+
}) }), showSummary ? (_jsx(Text, { color: textMuted, children: a(truncate(`${library.recent.length} recent · ${library.favorites.length} favorites · ${library.imported.length} imported`, width)) })) : null, gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null] }));
|
|
192
202
|
}
|
|
193
203
|
function AdaptiveList({ rows, selected, pageSize, width, theme, reduceMotion }) {
|
|
194
204
|
const selectedIndex = Math.min(Math.max(selected, 0), Math.max(0, rows.length - 1));
|
|
@@ -201,35 +211,37 @@ function AdaptiveList({ rows, selected, pageSize, width, theme, reduceMotion })
|
|
|
201
211
|
const detail = row.detail ? `${row.separator ?? ' · '}${row.detail}` : '';
|
|
202
212
|
const text = `${row.label}${detail}`;
|
|
203
213
|
const favoriteWidth = row.favoriteGlyph ? 2 : 0;
|
|
204
|
-
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));
|
|
214
|
+
return (_jsxs(Text, { color: active ? accent : row.heading ? textMuted : undefined, bold: active || row.heading, "aria-label": `${active ? 'Selected: ' : ''}${text}${row.favoriteGlyph ? '. Favorite' : ''}`, 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));
|
|
205
215
|
}) }));
|
|
206
216
|
}
|
|
207
217
|
function StaticRows({ rows, width, theme }) {
|
|
208
218
|
const accent = themeAccent(theme);
|
|
209
|
-
|
|
219
|
+
const { ascii } = useDisplay();
|
|
220
|
+
return (_jsx(Box, { flexDirection: "column", children: rows.map(row => (_jsxs(Text, { color: row.heading ? textMuted : undefined, bold: row.heading, "aria-label": row.detail ? `${row.label}: ${row.detail}` : row.label, children: [ascii ? toAsciiSafe(truncate(row.detail ? `${row.label} · ${row.detail}` : row.label, width)) : truncate(row.detail ? `${row.label} · ${row.detail}` : row.label, width), row.key === 'activity' ? _jsx(Text, { color: accent }) : null] }, row.key))) }));
|
|
210
221
|
}
|
|
211
222
|
function AdaptiveNowPlaying({ mode, station, playback, metadata, width, height, ascii, theme, receiverStyle, reduceMotion }) {
|
|
212
223
|
const pulse = useReceiverPulse();
|
|
224
|
+
const { screenReader } = useDisplay();
|
|
213
225
|
const accent = themeAccent(theme);
|
|
214
226
|
const stationName = station?.name ?? 'No station tuned';
|
|
215
|
-
const showMetadata = mode === 'compact' && height >= 9
|
|
227
|
+
const showMetadata = Boolean(metadata?.title) && (screenReader || mode === 'compact' && height >= 9);
|
|
216
228
|
const headerRows = mode === 'compact' ? 2 : 1;
|
|
217
229
|
const metadataRows = showMetadata ? 1 : 0;
|
|
218
230
|
const gapRows = height >= 5 ? 1 : 0;
|
|
219
231
|
const availableVisualRows = Math.max(1, height - headerRows - metadataRows - gapRows * 2);
|
|
220
232
|
const visualHeight = visualizerHeight(receiverStyle, availableVisualRows, width);
|
|
221
|
-
const visualRows = buildVisualizer(receiverStyle, pulse, width, visualHeight, station, playback, theme, mode === 'micro' ? 'micro' : 'standard');
|
|
233
|
+
const visualRows = screenReader ? [] : buildVisualizer(receiverStyle, pulse, width, visualHeight, station, playback, theme, mode === 'micro' ? 'micro' : 'standard');
|
|
222
234
|
const header = mode === 'micro'
|
|
223
235
|
? `${stationName} · ${playback.state}`
|
|
224
236
|
: 'RADIOCLI Now playing';
|
|
225
237
|
const status = `${stationName} ${playback.state}${playback.muted ? ' · muted' : ` · vol ${playback.volume}`}`;
|
|
226
|
-
return (_jsxs(Box, { flexDirection: "column", height: height, width: width, overflow: "hidden", children: [_jsx(Text, { color: accent, bold: true, children: mode === 'micro'
|
|
238
|
+
return (_jsxs(Box, { flexDirection: "column", height: height, width: width, overflow: "hidden", children: [_jsx(Text, { color: accent, bold: true, "aria-label": mode === 'micro' ? `Now playing. Station: ${stationName}. Playback: ${playback.state}, ${playback.muted ? 'muted' : `volume ${playback.volume}`}` : undefined, children: mode === 'micro'
|
|
227
239
|
? _jsx(AdaptiveMarquee, { text: header, width: width, active: true, reduceMotion: reduceMotion })
|
|
228
|
-
: 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
|
|
240
|
+
: truncate(header, width) }), mode === 'compact' ? _jsx(Text, { color: textMuted, "aria-label": `Station: ${stationName}. Playback: ${playback.state}, ${playback.muted ? 'muted' : `volume ${playback.volume}`}`, children: ascii ? toAsciiSafe(truncate(status, width)) : truncate(status, width) }) : null, gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null, !screenReader ? _jsx(Box, { flexDirection: "column", height: availableVisualRows, overflow: "hidden", "aria-hidden": true, children: visualRows.map((row, index) => (_jsx(Text, { color: row.segments ? undefined : row.color, children: row.segments
|
|
229
241
|
? renderAdaptiveSegments(row.segments, ascii)
|
|
230
242
|
: ascii
|
|
231
243
|
? toAsciiSafe(row.text)
|
|
232
|
-
: 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] }));
|
|
244
|
+
: row.text }, index))) }) : null, showMetadata ? (_jsx(Text, { color: accent, "aria-label": `Track: ${metadata?.title ?? ''}`, children: _jsx(AdaptiveMarquee, { text: metadata?.title ?? '', width: width, active: true, reduceMotion: reduceMotion }) })) : null, gapRows ? _jsx(Box, { height: gapRows, flexShrink: 0 }) : null] }));
|
|
233
245
|
}
|
|
234
246
|
function renderAdaptiveSegments(segments, ascii) {
|
|
235
247
|
let offset = 0;
|
|
@@ -242,12 +254,13 @@ function renderAdaptiveSegments(segments, ascii) {
|
|
|
242
254
|
function AdaptiveSearch({ mode, query, editing, loading, rows, empty, selected, height, width, theme, ascii, reduceMotion }) {
|
|
243
255
|
const accent = themeAccent(theme);
|
|
244
256
|
const { panel: panelBackground } = useDisplay();
|
|
257
|
+
const a = (value) => ascii ? toAsciiSafe(value) : value;
|
|
245
258
|
const fieldText = query || 'station, genre, or place';
|
|
246
259
|
const prefix = loading ? (ascii ? '* ' : '⣾ ') : editing ? '› ' : '/ ';
|
|
247
260
|
const fieldRows = height >= 4 ? 3 : 1;
|
|
248
261
|
const gapRows = height >= 6 ? 1 : 0;
|
|
249
262
|
const listRows = Math.max(0, height - 1 - fieldRows - gapRows * 2);
|
|
250
|
-
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] }));
|
|
263
|
+
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: a(prefix) }), _jsx(Text, { color: query ? accent : textMuted, children: a(truncate(fieldText, Math.max(1, width - 5))) })] })) : (_jsx(Text, { color: query ? accent : textMuted, children: a(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] }));
|
|
251
264
|
}
|
|
252
265
|
function adaptiveRows(input) {
|
|
253
266
|
const { screen, stations, countries, airPlayDevices, library, diagnostics, backends, updateCheck, favoriteKeys, selected, width, mode, ascii } = input;
|
|
@@ -255,16 +268,20 @@ function adaptiveRows(input) {
|
|
|
255
268
|
return homeItems.map(item => ({
|
|
256
269
|
key: item.screen,
|
|
257
270
|
label: item.label,
|
|
258
|
-
detail: mode === 'compact' && width >= 52 ? item.detail : undefined
|
|
271
|
+
detail: mode === 'compact' && width >= 52 ? item.detail : undefined,
|
|
272
|
+
separator: ' '
|
|
259
273
|
}));
|
|
260
274
|
}
|
|
261
275
|
if (screen === 'settings') {
|
|
262
|
-
const
|
|
276
|
+
const pageItems = settingsItemsForPage(input.settingsPage);
|
|
277
|
+
const labels = pageItems.map(item => settingLabel(item, updateCheck, input.appVersion, input.airPlaySupported));
|
|
263
278
|
const labelWidth = pairedColumnWidth(labels, width, mode);
|
|
264
|
-
return
|
|
279
|
+
return pageItems.map((item, index) => ({
|
|
265
280
|
key: item,
|
|
266
|
-
label: padDisplayEnd(truncate(settingLabel(item, updateCheck), labelWidth), labelWidth),
|
|
267
|
-
detail:
|
|
281
|
+
label: padDisplayEnd(truncate(settingLabel(item, updateCheck, input.appVersion, input.airPlaySupported), labelWidth), labelWidth),
|
|
282
|
+
detail: input.settingsPage === 'root'
|
|
283
|
+
? adaptiveSettingsRootValue(item)
|
|
284
|
+
: settingValue(item, library.settings, diagnostics, backends, airPlayDevices, updateCheck, input.appVersion),
|
|
268
285
|
separator: ' ',
|
|
269
286
|
index
|
|
270
287
|
}));
|
|
@@ -303,6 +320,10 @@ function adaptiveRows(input) {
|
|
|
303
320
|
}
|
|
304
321
|
return stationAdaptiveRows(stations, favoriteKeys, selected, width, ascii);
|
|
305
322
|
}
|
|
323
|
+
function adaptiveSettingsRootValue(item) {
|
|
324
|
+
const group = settingsGroups.find(candidate => candidate.label === item);
|
|
325
|
+
return group ? `${group.items.length} settings ›` : undefined;
|
|
326
|
+
}
|
|
306
327
|
function stationAdaptiveRows(stations, favoriteKeys, selected, width, ascii) {
|
|
307
328
|
return stations.map((station, index) => {
|
|
308
329
|
const favorite = favoriteKeys.has(`${station.provider}:${station.id}`);
|
|
@@ -362,7 +383,7 @@ function adaptiveEmptyState(input) {
|
|
|
362
383
|
return [{ key: 'empty', label: `No stations in ${stationTitle.toLowerCase()}.` }, { key: 'hint', label: 'Try another view or clear filters.' }];
|
|
363
384
|
}
|
|
364
385
|
function adaptiveStatus(props) {
|
|
365
|
-
const { screen, playback, playingStation, stations, countries, searchQuery, editingSearch, countryFilter, editingCountryFilter, library, filterLabel
|
|
386
|
+
const { screen, playback, playingStation, stations, countries, searchQuery, editingSearch, countryFilter, editingCountryFilter, library, filterLabel } = props;
|
|
366
387
|
if (screen === 'home') {
|
|
367
388
|
return `${library.favorites.length} favorites · ${library.recent.length} recent · ${library.imported.length} imported`;
|
|
368
389
|
}
|
|
@@ -370,7 +391,10 @@ function adaptiveStatus(props) {
|
|
|
370
391
|
return `${playbackBackendLabel(playback.backend)} · ${playback.state} · vol ${playback.volume}`;
|
|
371
392
|
}
|
|
372
393
|
if (screen === 'settings') {
|
|
373
|
-
|
|
394
|
+
const group = settingsGroup(props.settingsPage ?? 'root');
|
|
395
|
+
return group
|
|
396
|
+
? `${group.label} · Enter changes setting · b categories`
|
|
397
|
+
: 'Choose a category · Updates are available here';
|
|
374
398
|
}
|
|
375
399
|
if (screen === 'search') {
|
|
376
400
|
const query = searchQuery || (editingSearch ? 'type to search' : 'press / to search');
|