@ciphore/radiocli 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/README.md +30 -0
  3. package/dist/agent/alarm-service.js +210 -0
  4. package/dist/agent/cli.js +193 -0
  5. package/dist/agent/headless-host.js +143 -0
  6. package/dist/agent/launcher.js +71 -0
  7. package/dist/agent/mcp-install.js +467 -0
  8. package/dist/agent/mcp-server.js +139 -0
  9. package/dist/agent/service.js +347 -0
  10. package/dist/agent/session.js +248 -0
  11. package/dist/alarms/cli.js +4 -1
  12. package/dist/alarms/runner.js +43 -26
  13. package/dist/cli.js +60 -3
  14. package/dist/player/player-controller.js +19 -0
  15. package/dist/providers/provider-manager.js +5 -0
  16. package/dist/providers/radio-browser.js +4 -0
  17. package/dist/setup.js +71 -2
  18. package/dist/storage/store.js +33 -1
  19. package/dist/types.js +6 -0
  20. package/dist/ui/AdaptiveContent.js +24 -9
  21. package/dist/ui/App.js +299 -18
  22. package/dist/ui/AppContent.js +4 -4
  23. package/dist/ui/components/StationList.js +3 -5
  24. package/dist/ui/components/VersionIndicator.js +19 -0
  25. package/dist/ui/page-footer.js +4 -2
  26. package/dist/ui/screen-items.js +40 -9
  27. package/dist/ui/screens/AlarmsScreen.js +2 -1
  28. package/dist/ui/screens/CountriesScreen.js +8 -5
  29. package/dist/ui/screens/HomeScreen.js +3 -1
  30. package/dist/ui/screens/SettingsScreen.js +70 -53
  31. package/dist/ui/use-alarm-tui.js +4 -0
  32. package/dist/ui/use-app-input.js +29 -3
  33. package/dist/ui/visualizers/gallop.js +118 -0
  34. package/dist/ui/visualizers/horse-stride.js +20 -0
  35. package/dist/ui/visualizers/receiver-style-registry.js +12 -2
  36. package/dist/ui/visualizers/receiver-visualizers.js +3 -0
  37. package/dist/ui/visualizers/retro-receivers.js +4 -0
  38. package/dist/ui/visualizers/terminal-receivers.js +57 -0
  39. package/dist/update-check.js +26 -7
  40. package/package.json +4 -1
@@ -11,7 +11,9 @@ export const homeItems = [
11
11
  ];
12
12
  export const settingsGroups = [
13
13
  {
14
- label: 'Playback',
14
+ id: 'playback',
15
+ label: 'Playback & audio',
16
+ description: 'Output, volume, AirPlay, and startup behavior',
15
17
  items: [
16
18
  'Audio output',
17
19
  'AirPlay receiver',
@@ -23,7 +25,9 @@ export const settingsGroups = [
23
25
  ]
24
26
  },
25
27
  {
26
- label: 'Display',
28
+ id: 'appearance',
29
+ label: 'Appearance',
30
+ description: 'Colors, receiver style, motion, and terminal display',
27
31
  items: [
28
32
  'Cycle display color',
29
33
  'Cycle receiver style',
@@ -34,22 +38,40 @@ export const settingsGroups = [
34
38
  ]
35
39
  },
36
40
  {
37
- label: 'Discovery & privacy',
41
+ id: 'discovery',
42
+ label: 'Discovery & providers',
43
+ description: 'Location, station directories, privacy, and provider health',
38
44
  items: [
39
45
  'Toggle nearby location lookup',
40
46
  'Toggle Radio Garden experimental adapter',
41
- 'Share favorite votes with Radio Browser'
47
+ 'Share favorite votes with Radio Browser',
48
+ 'Refresh provider health'
42
49
  ]
43
50
  },
44
51
  {
45
- label: 'Data',
52
+ id: 'data',
53
+ label: 'Data & library',
54
+ description: 'Back up or restore preferences and saved stations',
46
55
  items: [
47
56
  'Export preferences and library',
48
57
  'Import preferences and library'
49
58
  ]
50
59
  },
51
60
  {
61
+ id: 'agent',
62
+ label: 'Agent control & MCP',
63
+ description: 'Local agent access and MCP playback behavior',
64
+ items: [
65
+ 'Allow local agent control',
66
+ 'Install or repair MCP integrations',
67
+ 'Open TUI for agent playback',
68
+ 'Show Now Playing for agent playback'
69
+ ]
70
+ },
71
+ {
72
+ id: 'media-keys',
52
73
  label: 'Media keys',
74
+ description: 'Teach RadioCLI your keyboard playback controls',
53
75
  items: [
54
76
  'Learn previous media key',
55
77
  'Learn play/pause media key',
@@ -58,14 +80,23 @@ export const settingsGroups = [
58
80
  ]
59
81
  },
60
82
  {
61
- label: 'Maintenance',
83
+ id: 'updates',
84
+ label: 'Updates',
85
+ description: 'Automatic launch checks, version status, and installation',
62
86
  items: [
63
- 'Refresh provider health',
87
+ 'Automatically check for updates',
64
88
  'Check for updates'
65
89
  ]
66
90
  }
67
91
  ];
68
92
  export const settingsItems = settingsGroups.flatMap(group => group.items);
69
- export function settingsSectionFor(item) {
70
- return settingsGroups.find(group => group.items.some(candidate => candidate === item))?.label ?? 'Preferences';
93
+ export const settingsRootItems = settingsGroups.map(group => group.label);
94
+ export function settingsGroup(page) {
95
+ return settingsGroups.find(group => group.id === page);
96
+ }
97
+ export function settingsItemsForPage(page) {
98
+ return page === 'root' ? settingsRootItems : settingsGroup(page)?.items ?? [];
99
+ }
100
+ export function settingsPageForRootItem(item) {
101
+ return settingsGroups.find(group => group.label === item)?.id;
71
102
  }
@@ -76,12 +76,13 @@ export function AlarmRingingScreen({ sessions, alarms = [], selected, snoozeMinu
76
76
  const accent = themeAccent(theme);
77
77
  const session = sessions[selected];
78
78
  const label = session ? alarms.find(alarm => alarm.id === session.status.alarmId)?.label : undefined;
79
+ const starting = session?.status.state === 'starting';
79
80
  const { ascii } = useDisplay();
80
81
  const a = (value) => ascii ? toAsciiSafe(value) : value;
81
82
  const roomy = width >= 42 && height >= 12;
82
83
  const clockText = session ? formatClock(new Date(session.status.scheduledAt)) : '';
83
84
  const displayClock = roomy && !ascii ? fullwidthClock(clockText) : clockText;
84
- return _jsxs(Box, { flexDirection: "column", height: height, width: width, overflow: "hidden", children: [_jsx(ScreenHeader, { title: "Alarm ringing", subtitle: sessions.length > 1 ? `${selected + 1} of ${sessions.length} active alarms` : 'Wake radio · playing now', theme: theme, width: width }), session ? _jsxs(Box, { flexDirection: "column", alignItems: "center", width: width, children: [_jsx(Text, { backgroundColor: accent, color: "black", bold: true, children: a(' ALARM ACTIVE ') }), _jsx(Box, { marginTop: roomy ? 1 : 0, children: _jsx(Text, { backgroundColor: roomy ? accent : undefined, color: roomy ? 'black' : accent, bold: true, children: a(roomy ? ` ${displayClock} ` : displayClock) }) }), _jsxs(Box, { flexDirection: "column", alignItems: "center", marginTop: roomy ? 1 : 0, width: width, children: [_jsx(Text, { color: accent, bold: true, children: a(truncate(label ? `${session.status.stationName} · ${label}` : session.status.stationName, width)) }), height >= 12 ? _jsx(Text, { color: textMuted, children: a(truncate(`Started ${formatElapsed(session.status.startedAt)} ago · scheduled ${formatWhen(new Date(session.status.scheduledAt))}`, width)) }) : null] }), _jsxs(Box, { marginTop: 1, flexDirection: width >= 58 ? 'row' : 'column', alignItems: "center", children: [_jsx(Text, { backgroundColor: accent, color: "black", bold: true, children: a(' ENTER KEEP PLAYING ') }), width >= 58 ? _jsx(Text, { children: " " }) : null, _jsx(Text, { backgroundColor: accent, color: "black", bold: true, children: a(` SPACE SNOOZE ${snoozeMinutes} MIN `) })] }), height >= 14 ? _jsx(Text, { color: textMuted, children: a('Enter opens Playing · Space stops the radio until snooze ends · b return') }) : null, sessions.length > 1 && height >= 15 ? _jsx(Text, { color: textMuted, children: a(`↑/↓ choose active alarm · ${selected + 1}/${sessions.length}`) }) : null] }) : _jsx(Text, { color: textMuted, children: "The alarm session ended. Press b to return." })] });
85
+ return _jsxs(Box, { flexDirection: "column", height: height, width: width, overflow: "hidden", children: [_jsx(ScreenHeader, { title: "Alarm ringing", subtitle: sessions.length > 1 ? `${selected + 1} of ${sessions.length} active alarms` : starting ? 'Wake radio · preparing station' : 'Wake radio · playing now', theme: theme, width: width }), session ? _jsxs(Box, { flexDirection: "column", alignItems: "center", width: width, children: [_jsx(Text, { backgroundColor: accent, color: "black", bold: true, children: a(starting ? ' ALARM STARTING ' : ' ALARM ACTIVE ') }), _jsx(Box, { marginTop: roomy ? 1 : 0, children: _jsx(Text, { backgroundColor: roomy ? accent : undefined, color: roomy ? 'black' : accent, bold: true, children: a(roomy ? ` ${displayClock} ` : displayClock) }) }), _jsxs(Box, { flexDirection: "column", alignItems: "center", marginTop: roomy ? 1 : 0, width: width, children: [_jsx(Text, { color: accent, bold: true, children: a(truncate(label ? `${session.status.stationName} · ${label}` : session.status.stationName, width)) }), height >= 12 ? _jsx(Text, { color: textMuted, children: a(truncate(`${starting ? 'Preparing' : 'Started'} ${formatElapsed(session.status.startedAt)} ago · scheduled ${formatWhen(new Date(session.status.scheduledAt))}`, width)) }) : null] }), _jsxs(Box, { marginTop: 1, flexDirection: width >= 58 ? 'row' : 'column', alignItems: "center", children: [_jsx(Text, { backgroundColor: accent, color: "black", bold: true, children: a(starting ? ' ENTER WAIT FOR STATION ' : ' ENTER KEEP PLAYING ') }), width >= 58 ? _jsx(Text, { children: " " }) : null, _jsx(Text, { backgroundColor: accent, color: "black", bold: true, children: a(` SPACE SNOOZE ${snoozeMinutes} MIN `) })] }), height >= 14 ? _jsx(Text, { color: textMuted, children: a(starting ? 'Space snoozes before playback · b return' : 'Enter opens Playing · Space stops the radio until snooze ends · b return') }) : null, sessions.length > 1 && height >= 15 ? _jsx(Text, { color: textMuted, children: a(`↑/↓ choose active alarm · ${selected + 1}/${sessions.length}`) }) : null] }) : _jsx(Text, { color: textMuted, children: "The alarm session ended. Press b to return." })] });
85
86
  }
86
87
  function editorRows(draft) {
87
88
  const next = draftNextOccurrence(draft);
@@ -4,16 +4,19 @@ import { Menu, Pointer } from '../components/Menu.js';
4
4
  import { ScreenHeader } from '../components/ScreenHeader.js';
5
5
  import { textMuted, themeAccent } from '../theme.js';
6
6
  import { visibleWindow } from '../list-window.js';
7
- import { displayWidth, truncate } from '../format.js';
8
- export function CountriesScreen({ countries, selected, loading, filter, editingFilter, theme, pageSize, width }) {
7
+ import { displayWidth } from '../format.js';
8
+ import { AdaptiveMarquee } from '../components/AdaptiveMarquee.js';
9
+ export function CountriesScreen({ countries, selected, loading, filter, editingFilter, theme, pageSize, width, reduceMotion = false }) {
9
10
  const window = visibleWindow(countries, selected, pageSize);
10
11
  const rowWidth = Math.max(24, width - 2);
11
12
  const countWidth = Math.max(12, Math.min(22, Math.max(...countries.map(country => `${country.stationCount.toLocaleString()} stations`.length), 12)));
12
- const visibleCountryWidth = Math.max(8, ...window.items.map(country => displayWidth(`${country.name} (${country.code})`)));
13
- const countryWidth = Math.min(42, visibleCountryWidth, Math.max(8, rowWidth - countWidth - 4));
13
+ // Keep the metadata column fixed while the viewport scrolls. Forty-two cells
14
+ // accommodates long directory names such as Lao People's Democratic Republic
15
+ // plus its country code, while narrow terminals still receive a bounded row.
16
+ const countryWidth = Math.min(42, Math.max(8, rowWidth - countWidth - 4));
14
17
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScreenHeader, { title: "Countries", subtitle: editingFilter ? 'Filtering countries — type to narrow the list' : 'Browse the worldwide country directory', width: width, theme: theme, right: filter ? `filter: ${filter}` : undefined }), loading ? _jsx(Text, { color: textMuted, children: "Loading countries from Radio Browser\u2026" }) : null, !loading ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: textMuted, children: ["Showing ", countries.length ? window.start + 1 : 0, "-", window.end, " of ", countries.length] }), _jsx(Menu, { items: window.items, selected: selected - window.start, keyFor: country => country.code, render: (country, _index, active) => {
15
18
  const code = ` (${country.code})`;
16
19
  const nameWidth = Math.max(3, countryWidth - displayWidth(code));
17
- return (_jsxs(Box, { height: 1, width: rowWidth, children: [_jsx(Pointer, { active: active }), _jsxs(Box, { width: countryWidth, children: [_jsx(Text, { color: active ? themeAccent(theme) : undefined, bold: active, children: truncate(country.name, nameWidth) }), _jsx(Text, { color: textMuted, children: code })] }), _jsx(Box, { width: 2 }), _jsx(Box, { width: countWidth, children: _jsxs(Text, { color: textMuted, children: [country.stationCount.toLocaleString(), " stations"] }) })] }));
20
+ return (_jsxs(Box, { height: 1, width: rowWidth, children: [_jsx(Pointer, { active: active }), _jsxs(Box, { width: countryWidth, children: [_jsx(Text, { color: active ? themeAccent(theme) : undefined, bold: active, children: _jsx(AdaptiveMarquee, { text: country.name, width: nameWidth, active: active, reduceMotion: reduceMotion }) }), _jsx(Text, { color: textMuted, children: code })] }), _jsx(Box, { width: 2 }), _jsx(Box, { width: countWidth, children: _jsxs(Text, { color: textMuted, children: [country.stationCount.toLocaleString(), " stations"] }) })] }));
18
21
  } })] })) : null] }));
19
22
  }
@@ -4,6 +4,8 @@ import { Logo } from '../components/Logo.js';
4
4
  import { Menu, Pointer } from '../components/Menu.js';
5
5
  import { textMuted, themeAccent } from '../theme.js';
6
6
  import { homeItems } from '../screen-items.js';
7
+ import { displayWidth } from '../format.js';
7
8
  export function HomeScreen({ selected, theme, library }) {
8
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Logo, {}), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Menu, { items: homeItems, selected: selected, keyFor: item => item.screen, render: (item, index, active) => (_jsxs(Box, { children: [_jsx(Pointer, { active: active }), _jsxs(Text, { color: textMuted, children: [index + 1, " "] }), _jsx(Text, { color: active ? themeAccent(theme) : undefined, bold: active, children: item.label }), _jsxs(Text, { color: textMuted, children: [" \u00B7 ", item.detail] })] })) }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: textMuted, children: [library.recent.length, " recent \u00B7 ", library.favorites.length, " favorites \u00B7 ", library.imported.length, " imported"] }) })] }));
9
+ const titleWidth = Math.max(...homeItems.map(item => displayWidth(item.label))) + 2;
10
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Logo, {}), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Menu, { items: homeItems, selected: selected, keyFor: item => item.screen, render: (item, index, active) => (_jsxs(Box, { children: [_jsx(Pointer, { active: active }), _jsxs(Text, { color: textMuted, children: [index + 1, " "] }), _jsx(Box, { width: titleWidth, children: _jsx(Text, { color: active ? themeAccent(theme) : undefined, bold: active, children: item.label }) }), _jsxs(Text, { color: textMuted, children: [" ", item.detail] })] })) }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: textMuted, children: [library.recent.length, " recent \u00B7 ", library.favorites.length, " favorites \u00B7 ", library.imported.length, " imported"] }) })] }));
9
11
  }
@@ -1,73 +1,68 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
+ import { defaultAgentControlSettings } from '../../types.js';
3
4
  import { Menu, Pointer } from '../components/Menu.js';
4
5
  import { ScreenHeader } from '../components/ScreenHeader.js';
5
6
  import { truncate } from '../format.js';
6
- import { settingsItems, settingsSectionFor } from '../screen-items.js';
7
+ import { settingsGroup, settingsGroups, settingsItemsForPage } from '../screen-items.js';
7
8
  import { textMuted, themeAccent } from '../theme.js';
8
9
  import { playbackBackendCapabilities } from '../../player/backend-install.js';
9
10
  import { airPlayReceiverSettingValue } from '../airplay-settings.js';
10
11
  import { audioOutputLabel, audioOutputSettingValue } from '../audio-output.js';
11
12
  import { updateStatusText } from '../../update-check.js';
12
- export function SettingsScreen({ selected, settings, appVersion, updateCheck, storePath, playback, backends, airPlayDevices, providerHealth, theme, diagnostics, width, height }) {
13
+ export function SettingsScreen({ page = 'root', selected, settings, appVersion, updateCheck, storePath, playback, backends, airPlayDevices, providerHealth, theme, diagnostics, width, height }) {
13
14
  const accent = themeAccent(theme);
14
15
  const lineWidth = Math.max(32, width - 4);
15
- const menuWindow = settingsMenuWindow(selected, height);
16
- const selectedItem = settingsItems[selected];
17
- const activeSection = settingsSectionFor(selectedItem);
18
- const detail = settingDetail({
19
- item: selectedItem,
20
- appVersion,
21
- updateCheck,
22
- storePath,
23
- playback,
24
- providerHealth,
25
- diagnostics,
26
- lineWidth
27
- });
16
+ const group = settingsGroup(page);
17
+ const pageItems = settingsItemsForPage(page);
18
+ const menuWindow = settingsMenuWindow(pageItems, selected, height);
19
+ const selectedItem = pageItems[selected];
20
+ const detail = page === 'root' && selectedItem !== 'Check for updates'
21
+ ? settingsGroupsDescription(selectedItem)
22
+ : settingDetail({
23
+ item: selectedItem,
24
+ appVersion,
25
+ updateCheck,
26
+ storePath,
27
+ playback,
28
+ providerHealth,
29
+ diagnostics,
30
+ lineWidth
31
+ });
28
32
  const valueGap = 3;
29
33
  const labelWidth = Math.min(34, Math.max(20, lineWidth - valueGap - 24));
30
34
  const valueWidth = Math.max(12, Math.min(34, lineWidth - labelWidth - valueGap - 2));
31
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScreenHeader, { title: "Settings", subtitle: activeSection, width: width, theme: theme }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Menu, { items: menuWindow.items, selected: menuWindow.selectedOffset, keyFor: ({ item }) => item, render: (item, _index, active) => {
32
- const label = settingLabel(item.item, updateCheck);
33
- const value = settingValue(item.item, settings, diagnostics, backends, airPlayDevices, updateCheck);
34
- return (_jsxs(Box, { flexDirection: "column", children: [item.showSection ? (_jsx(Text, { color: textMuted, bold: true, children: item.section })) : null, _jsxs(Box, { width: lineWidth, children: [_jsx(Pointer, { active: active }), _jsx(Box, { width: labelWidth, children: _jsx(Text, { color: active ? accent : undefined, bold: active, children: truncate(label, labelWidth) }) }), _jsx(Box, { width: valueGap }), _jsx(Box, { width: valueWidth, children: _jsx(Text, { color: value ? accent : textMuted, children: truncate(value ?? '', valueWidth) }) })] })] }));
35
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScreenHeader, { title: "Settings", subtitle: group ? `Settings › ${group.label}` : 'Choose a category', width: width, theme: theme }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Menu, { items: menuWindow.items, selected: menuWindow.selectedOffset, keyFor: item => item, render: (item, _index, active) => {
36
+ const label = settingLabel(item, updateCheck, appVersion);
37
+ const value = page === 'root'
38
+ ? settingsRootValue(item)
39
+ : settingValue(item, settings, diagnostics, backends, airPlayDevices, updateCheck, appVersion);
40
+ return (_jsxs(Box, { width: lineWidth, children: [_jsx(Pointer, { active: active }), _jsx(Box, { width: labelWidth, children: _jsx(Text, { color: active ? accent : undefined, bold: active, children: truncate(label, labelWidth) }) }), _jsx(Box, { width: valueGap }), _jsx(Box, { width: valueWidth, children: _jsx(Text, { color: value ? accent : textMuted, children: truncate(value ?? '', valueWidth) }) })] }));
35
41
  } }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: textMuted, bold: true, children: "Selected" }), _jsx(Text, { color: textMuted, children: truncate(detail, lineWidth) })] })] }));
36
42
  }
37
- function settingsMenuWindow(selected, height) {
43
+ function settingsMenuWindow(items, selected, height) {
38
44
  const reservedRows = 6;
39
45
  const maxRows = height ? Math.max(5, height - reservedRows) : Number.POSITIVE_INFINITY;
40
- const clampedSelected = Math.min(Math.max(selected, 0), settingsItems.length - 1);
41
- const pages = [];
42
- let page = [];
43
- let pageRows = 0;
44
- let previousSection = '';
45
- settingsItems.forEach((item, index) => {
46
- const section = settingsSectionFor(item);
47
- let showSection = section !== previousSection;
48
- let rowCost = 1 + (showSection ? 1 : 0);
49
- if (page.length > 0 && pageRows + rowCost > maxRows) {
50
- pages.push(page);
51
- page = [];
52
- pageRows = 0;
53
- previousSection = '';
54
- showSection = true;
55
- rowCost = 2;
56
- }
57
- page.push({ item, section, showSection, index });
58
- pageRows += rowCost;
59
- previousSection = section;
60
- });
61
- if (page.length > 0) {
62
- pages.push(page);
63
- }
64
- const selectedPage = pages.find(candidate => candidate.some(entry => entry.index === clampedSelected)) ?? pages[0] ?? [];
46
+ const clampedSelected = Math.min(Math.max(selected, 0), items.length - 1);
47
+ const pageSize = Number.isFinite(maxRows) ? Math.max(1, Math.floor(maxRows)) : items.length;
48
+ const pageStart = Math.floor(clampedSelected / pageSize) * pageSize;
49
+ const selectedPage = items.slice(pageStart, pageStart + pageSize);
65
50
  return {
66
- items: selectedPage,
67
- selectedOffset: Math.max(0, selectedPage.findIndex(entry => entry.index === clampedSelected))
51
+ items: [...selectedPage],
52
+ selectedOffset: clampedSelected - pageStart
68
53
  };
69
54
  }
70
- export function settingValue(item, settings, diagnostics, backends, airPlayDevices, updateCheck) {
55
+ function settingsRootValue(item) {
56
+ const group = settingsGroupByLabel(item);
57
+ return group ? `${group.items.length} settings ›` : undefined;
58
+ }
59
+ function settingsGroupByLabel(label) {
60
+ return settingsGroups.find(group => group.label === label);
61
+ }
62
+ function settingsGroupsDescription(label) {
63
+ return label ? settingsGroupByLabel(label)?.description ?? 'Press Enter to open this category.' : '';
64
+ }
65
+ export function settingValue(item, settings, diagnostics, backends, airPlayDevices, updateCheck, currentVersion) {
71
66
  switch (item) {
72
67
  case 'Cycle display color':
73
68
  return settings.theme;
@@ -100,8 +95,18 @@ export function settingValue(item, settings, diagnostics, backends, airPlayDevic
100
95
  return settings.reduceMotion ? 'on' : 'off';
101
96
  case 'Mouse and trackpad scrolling':
102
97
  return settings.mouseSupport === false ? 'off' : 'auto';
98
+ case 'Automatically check for updates':
99
+ return settings.automaticUpdateChecks === false ? 'off' : 'on';
100
+ case 'Allow local agent control':
101
+ return (settings.agentControl ?? defaultAgentControlSettings).enabled ? 'on' : 'off';
102
+ case 'Install or repair MCP integrations':
103
+ return 'run repair';
104
+ case 'Open TUI for agent playback':
105
+ return (settings.agentControl ?? defaultAgentControlSettings).openUiOnPlay ? 'on' : 'off';
106
+ case 'Show Now Playing for agent playback':
107
+ return (settings.agentControl ?? defaultAgentControlSettings).focusNowPlaying ? 'on' : 'off';
103
108
  case 'Check for updates':
104
- return updateStatusText(updateCheck);
109
+ return updateStatusText(updateCheck, currentVersion);
105
110
  case 'Reset learned media keys':
106
111
  return `prev ${settings.mediaKeys.previous.length} · play ${settings.mediaKeys.playPause.length} · next ${settings.mediaKeys.next.length}`;
107
112
  case 'Export preferences and library':
@@ -112,9 +117,9 @@ export function settingValue(item, settings, diagnostics, backends, airPlayDevic
112
117
  return undefined;
113
118
  }
114
119
  }
115
- export function settingLabel(item, updateCheck) {
116
- if (item === 'Check for updates' && updateCheck?.updateAvailable) {
117
- return 'Install update';
120
+ export function settingLabel(item, updateCheck, currentVersion) {
121
+ if (item === 'Check for updates') {
122
+ return updateStatusText(updateCheck, currentVersion).endsWith('available') ? 'Install update' : 'Check now';
118
123
  }
119
124
  return settingDisplayLabels[item] ?? item;
120
125
  }
@@ -135,7 +140,10 @@ function settingDetail(input) {
135
140
  return health.length ? health.map(([provider, status]) => `${provider}: ${status}`).join(' · ') : 'Press Enter to check station providers.';
136
141
  }
137
142
  if (item === 'Check for updates') {
138
- return `RadioCLI v${appVersion} · ${updateStatusText(updateCheck)}`;
143
+ return `RadioCLI v${appVersion} · ${updateStatusText(updateCheck, appVersion)}`;
144
+ }
145
+ if (item === 'Automatically check for updates') {
146
+ return 'When enabled, RadioCLI checks the registry once whenever the app launches.';
139
147
  }
140
148
  if (item === 'Export preferences and library' || item === 'Import preferences and library') {
141
149
  return `Library file: ${truncate(storePath, Math.max(8, lineWidth - 14))}`;
@@ -149,5 +157,14 @@ function settingDetail(input) {
149
157
  if (item === 'Reduce motion') {
150
158
  return 'Freezes animated receiver displays for calmer, lower-power rendering.';
151
159
  }
160
+ if (item === 'Allow local agent control') {
161
+ return 'Press Enter to set up or remove agent and Codex Voice control across detected clients.';
162
+ }
163
+ if (item === 'Install or repair MCP integrations') {
164
+ return 'Checks and repairs every detected client. Restart open Codex or agent clients afterward.';
165
+ }
166
+ if (item === 'Open TUI for agent playback') {
167
+ return 'Opt in to a separate terminal window. On macOS, the agent app must approve a one-time Automation prompt.';
168
+ }
152
169
  return 'Press Enter to change this setting.';
153
170
  }
@@ -461,6 +461,10 @@ export function useAlarmTui(params) {
461
461
  if (!active)
462
462
  return false;
463
463
  if (key.return) {
464
+ if (active.status.state === 'starting') {
465
+ setMessage(`Still preparing ${active.status.stationName}; handoff will be available once playback starts.`);
466
+ return true;
467
+ }
464
468
  if (pendingActiveActionsRef.current.has(active.key)) {
465
469
  setMessage(`An alarm control request is already pending for ${active.status.stationName}.`);
466
470
  return true;
@@ -1,11 +1,11 @@
1
1
  import { useEffect } from 'react';
2
2
  import { useInput } from 'ink';
3
- import { homeItems, settingsItems } from './screen-items.js';
3
+ import { homeItems, settingsItemsForPage, settingsPageForRootItem } from './screen-items.js';
4
4
  import { applyTextInput, favoriteTarget, isEditableInput, isPlainPrintableInput, mediaTransportActionForInput, searchEditingArrowAction, shouldHandleKeyboardEvent, shouldToggleNearbyLocationShortcut } from './app-state.js';
5
5
  import { parseTerminalMouseEvents, primaryMousePress, wheelScrollDelta } from './terminal-mouse.js';
6
6
  import { completeCommand } from './help-content.js';
7
7
  import { commitImmediateSelection } from './selection-state.js';
8
- export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, cancelPendingAutoSkip, beginLearningTransportKey, capturingTransportAction, commandMode, commandText, confirmCtrlCExit, copyStationUrl, openStationHomepage, currentItemCount, cycleDisplayColor, cycleAudioOutput, cycleReceiverStyle, cycleSleepTimer, editingCountryFilter, editingSearch, executeCommand, filteredCountries, go, lastRawTransportAtRef, lastSubmittedSearchRef, loadCountry, openAdjacentTab, openAirPlayCode, openAirPlaySettings, openScreen, handleAlarmInput, openAlarmForStation, openActiveAlarms, playAdjacent, playStation, player, playingStation, recallSearchHistory, moveExploreCursor, moveExploreCursorToCell, refreshAirPlayTargets, refreshProviderHealth, resetLearnedTransportKeys, runSearch, saveLearnedTransportKey, screen, searchQuery, selectedRef, selectedStationForInput, selectAirPlayDeviceAt, setCapturingTransportAction, setAirPlayCode, setCommandMode, setCommandText, setCountryFilter, setEditingCountryFilter, setEditingSearch, setMessage, setSearchQuery, setSelected, setShowDiagnostics, submitAirPlayCode, settingsRef, shutdown, stdin, toggleFavorite, toggleMute, togglePause, toggleSetting, toggleNearbyLocation, toggleDirectoryVoting, toggleRadioGarden, toggleSkipBrokenStreams, updateFromSettings }) {
8
+ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, cancelPendingAutoSkip, beginLearningTransportKey, capturingTransportAction, commandMode, commandText, confirmCtrlCExit, copyStationUrl, openStationHomepage, currentItemCount, cycleDisplayColor, cycleAudioOutput, cycleReceiverStyle, cycleSleepTimer, editingCountryFilter, editingSearch, executeCommand, filteredCountries, go, lastRawTransportAtRef, lastSubmittedSearchRef, loadCountry, openAdjacentTab, openAirPlayCode, openAirPlaySettings, openSettingsPage, closeSettingsPage, openScreen, handleAlarmInput, openAlarmForStation, openActiveAlarms, playAdjacent, playStation, player, playingStation, recallSearchHistory, moveExploreCursor, moveExploreCursorToCell, refreshAirPlayTargets, refreshProviderHealth, resetLearnedTransportKeys, repairMcpIntegrations, setAgentIntegrationEnabled, runSearch, saveLearnedTransportKey, screen, settingsPage, searchQuery, selectedRef, selectedStationForInput, selectAirPlayDeviceAt, setCapturingTransportAction, setAirPlayCode, setCommandMode, setCommandText, setCountryFilter, setEditingCountryFilter, setEditingSearch, setMessage, setSearchQuery, setSelected, setShowDiagnostics, submitAirPlayCode, settingsRef, shutdown, stdin, toggleFavorite, toggleMute, togglePause, toggleSetting, toggleAgentSetting, toggleNearbyLocation, toggleDirectoryVoting, toggleRadioGarden, toggleSkipBrokenStreams, updateFromSettings }) {
9
9
  const commitSelection = (next) => {
10
10
  commitImmediateSelection(selectedRef, setSelected, next, currentItemCount(screen));
11
11
  };
@@ -338,6 +338,10 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
338
338
  go('settings');
339
339
  return;
340
340
  }
341
+ if ((input === 'b' || key.escape) && screen === 'settings') {
342
+ closeSettingsPage();
343
+ return;
344
+ }
341
345
  if (input === 'b' || key.escape) {
342
346
  if (screen === 'stations') {
343
347
  go('countries');
@@ -419,7 +423,14 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
419
423
  return;
420
424
  }
421
425
  if (screen === 'settings') {
422
- const item = settingsItems[selectedRef.current];
426
+ const item = settingsItemsForPage(settingsPage)[selectedRef.current];
427
+ if (settingsPage === 'root') {
428
+ const targetPage = settingsPageForRootItem(item);
429
+ if (targetPage) {
430
+ openSettingsPage(targetPage);
431
+ }
432
+ return;
433
+ }
423
434
  if (item === 'Cycle display color') {
424
435
  cycleDisplayColor();
425
436
  }
@@ -468,6 +479,21 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
468
479
  else if (item === 'Mouse and trackpad scrolling') {
469
480
  toggleSetting('mouseSupport');
470
481
  }
482
+ else if (item === 'Automatically check for updates') {
483
+ toggleSetting('automaticUpdateChecks');
484
+ }
485
+ else if (item === 'Allow local agent control') {
486
+ void setAgentIntegrationEnabled();
487
+ }
488
+ else if (item === 'Install or repair MCP integrations') {
489
+ void repairMcpIntegrations();
490
+ }
491
+ else if (item === 'Open TUI for agent playback') {
492
+ toggleAgentSetting('openUiOnPlay');
493
+ }
494
+ else if (item === 'Show Now Playing for agent playback') {
495
+ toggleAgentSetting('focusNowPlaying');
496
+ }
471
497
  else if (item === 'Export preferences and library') {
472
498
  setCommandText('export ');
473
499
  setCommandMode(true);
@@ -0,0 +1,118 @@
1
+ import { themeAccent } from '../theme.js';
2
+ import { horseStrideFrames, horseStrideHeight, horseStrideWidth } from './horse-stride.js';
3
+ const frames = horseStrideFrames.map(frame => Buffer.from(frame, 'base64'));
4
+ const brailleBits = [[1, 8], [2, 16], [4, 32], [64, 128]];
5
+ /** One pose per receiver tick. The same scale and torso anchor are used for every pose. */
6
+ export function buildGallop(pulse, width, height, theme) {
7
+ const w = Math.max(1, Math.floor(width));
8
+ const h = Math.max(1, Math.floor(height));
9
+ const color = themeAccent(theme);
10
+ const frameIndex = ((Math.floor(pulse) % frames.length) + frames.length) % frames.length;
11
+ const frame = frames[frameIndex];
12
+ const usableW = w > 8 ? w - 4 : w;
13
+ const usableH = h > 4 ? h - 2 : h;
14
+ const scale = Math.min(usableW * 2 / horseStrideWidth, usableH * 4 / horseStrideHeight);
15
+ const dotsW = Math.max(1, Math.floor(horseStrideWidth * scale));
16
+ const dotsH = Math.max(1, Math.floor(horseStrideHeight * scale));
17
+ const left = Math.floor((w - Math.ceil(dotsW / 2)) / 2) * 2;
18
+ const top = Math.floor((h - Math.ceil(dotsH / 4)) / 2) * 4;
19
+ const masks = new Uint8Array(w * h);
20
+ for (let y = 0; y < dotsH; y++) {
21
+ const sy0 = Math.floor(y * horseStrideHeight / dotsH);
22
+ const sy1 = Math.ceil((y + 1) * horseStrideHeight / dotsH);
23
+ for (let x = 0; x < dotsW; x++) {
24
+ const sx0 = Math.floor(x * horseStrideWidth / dotsW);
25
+ const sx1 = Math.ceil((x + 1) * horseStrideWidth / dotsW);
26
+ let ink = 0;
27
+ for (let sy = sy0; sy < sy1; sy++)
28
+ for (let sx = sx0; sx < sx1; sx++) {
29
+ const bit = sy * horseStrideWidth + sx;
30
+ ink += (frame[bit >> 3] >> (bit & 7)) & 1;
31
+ }
32
+ // Area coverage keeps thin legs continuous as the terminal becomes smaller.
33
+ if (ink / ((sx1 - sx0) * (sy1 - sy0)) < 0.22)
34
+ continue;
35
+ const dx = left + x;
36
+ const dy = top + y;
37
+ const index = Math.floor(dy / 4) * w + Math.floor(dx / 2);
38
+ masks[index] |= brailleBits[dy % 4][dx % 2];
39
+ }
40
+ }
41
+ return Array.from({ length: h }, (_, y) => {
42
+ let text = '';
43
+ for (let x = 0; x < w; x++) {
44
+ const mask = masks[y * w + x];
45
+ text += mask === 0 ? ' ' : String.fromCharCode(0x2800 + mask);
46
+ }
47
+ // No background color: only the horse's ink is drawn over the receiver panel.
48
+ return { text, color, segments: [{ text, color }] };
49
+ });
50
+ }
51
+ /** Lightly pare back thick stroke edges, keeping endpoints and junctions intact. */
52
+ export function buildGallopFine(pulse, width, height, theme) {
53
+ return thinGallopStrokes(buildGallop(pulse, width, height, theme), 0.14);
54
+ }
55
+ function thinGallopStrokes(original, reduction) {
56
+ const w = original[0].text.length;
57
+ const h = original.length;
58
+ const dw = w * 2;
59
+ const dh = h * 4;
60
+ const dots = new Uint8Array(dw * dh);
61
+ let inkCount = 0;
62
+ original.forEach((row, y) => {
63
+ [...row.text].forEach((glyph, x) => {
64
+ const mask = glyph === ' ' ? 0 : glyph.charCodeAt(0) - 0x2800;
65
+ for (let yy = 0; yy < 4; yy++)
66
+ for (let xx = 0; xx < 2; xx++) {
67
+ if (mask & brailleBits[yy][xx]) {
68
+ dots[(y * 4 + yy) * dw + x * 2 + xx] = 1;
69
+ inkCount++;
70
+ }
71
+ }
72
+ });
73
+ });
74
+ const neighbors = [-dw, -dw + 1, 1, dw + 1, dw, dw - 1, -1, -dw - 1];
75
+ const canThin = (index) => {
76
+ if (!dots[index])
77
+ return false;
78
+ const ring = neighbors.map(offset => dots[index + offset]);
79
+ const count = ring.reduce((total, dot) => total + dot, 0);
80
+ // Already-fine lines and endpoints stay untouched. A single connected arc of
81
+ // neighbors ensures removing this edge dot cannot split a leg or contour.
82
+ if (count < 3 || count > 6)
83
+ return false;
84
+ let transitions = 0;
85
+ for (let i = 0; i < 8; i++)
86
+ if (!ring[i] && ring[(i + 1) % 8])
87
+ transitions++;
88
+ return transitions === 1;
89
+ };
90
+ const candidates = [];
91
+ for (let y = 1; y < dh - 1; y++)
92
+ for (let x = 1; x < dw - 1; x++) {
93
+ const index = y * dw + x;
94
+ if (canThin(index))
95
+ candidates.push(index);
96
+ }
97
+ const budget = Math.min(candidates.length, Math.floor(inkCount * reduction));
98
+ for (let i = 0; i < budget; i++) {
99
+ // Spread the small reduction over the entire horse instead of thinning its
100
+ // head first. Recheck after each removal to preserve connected strokes.
101
+ const index = candidates[Math.floor(i * candidates.length / budget)];
102
+ if (canThin(index))
103
+ dots[index] = 0;
104
+ }
105
+ return original.map((row, y) => {
106
+ let text = '';
107
+ for (let x = 0; x < w; x++) {
108
+ let mask = 0;
109
+ for (let yy = 0; yy < 4; yy++)
110
+ for (let xx = 0; xx < 2; xx++) {
111
+ if (dots[(y * 4 + yy) * dw + x * 2 + xx])
112
+ mask |= brailleBits[yy][xx];
113
+ }
114
+ text += mask ? String.fromCharCode(0x2800 + mask) : ' ';
115
+ }
116
+ return { text, color: row.color, segments: [{ text, color: row.color }] };
117
+ });
118
+ }
@@ -0,0 +1,20 @@
1
+ /** Fixed-canvas gallop poses derived from the user-provided horse line-art reference.
2
+ * Each base64 frame is a 128 × 80 one-bit ink mask, packed least-significant bit first.
3
+ * Background pixels are transparent; no video decoder or source file is needed at runtime.
4
+ */
5
+ export const horseStrideWidth = 128;
6
+ export const horseStrideHeight = 80;
7
+ export const horseStrideFrames = [
8
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAOB9AAAAAAAAAAAAAAAAAADwMQAAAAAAAAAAAAAAAAAADzADAAAAAAAAAAAAAAAAwA8mBwAAAAAAAAAAAAAAAPCAAxwAAAAAAAAAAAAAAAA4wAE4AAAAAAAAAAAAAAAAmGcAcAAAAAAAAAAAAAAAAIg5APgAAAAAAAAAAAAAAAAMOGCAAQAAAAAAAAAAAAAABgxgIAMAAAAAAAAAAAAAgAMGYAAIAAAAAAAAAAAAAMABA2AAGAAAAAAAAAAAAADgxwPAABAAAAAAAAAAAAAA8OMA4A8QAAAAAAAAAAAAAPB8AGB+EAAAAAAAAAAAAADgHgAw4BAAAAAAAAAAAAAAwAcAEIARAAAAAAAAAID/AcABABgAAQAAAAAAAADw//9hgAAYAAAAAAAAAAAAeAD8AQABDAAAAAAAAAAAAB4AAAAAAAwAAAAAAAAAAAAHEAAAAAAMAAAAAAAAAACAAxAAAAAGDAAAAAAAAAAAgAEAAAAADAYAAAAAAAAAADgAAAAAABgGAAAAAAAAAMAfAAAAAAA4BgAAAAAAAAD/AwAAAAAAMAIAAAAAAADg/wMAAAAAAHAHAAAAAAAAMIBgAAAAAADwBwAAAAAAAAjwYADAAAAA8AcAAAAAAAAAOOAAwAAAAPAGAAAAAAAAAAjAEMAAAAD4DgAAAAAAAAAIwHGAAAAA/DwAAAAAAAAAAIBDgAAAAPwgAAAAAAAAAAAAB4AAAAB9AA8AAAAABgAAAB+AAACCfQAeAAAA5gwAAAB+gAAAxngAGAAAAIwBgAAA+MADAObw/zEAAAAYAIAAALCB/wB+YK8HAAAAcADAAADggf//f+AADgAAAMABYAAA4IP5f/DEAQwAAAAAPnAAAECDAQDAhwMYAAAAAD84AABAhgEAAB8HGAAAAIAfHwAAwIYAAAB8DtAAAAAA/QcAAMCGAAAA8BzQAAAAAPABAACAhAEAAMABsAEAAAAAAAAAgIYBAEyACbABAAAAAAAAAIAGAYDPwAlwAQAAAAAAAADAHgOA8/kAcAMAAAAAAAAAwD4cANt/APADAAAAAAAAAGD4+AC4PgDgAwAAAAAAAAAAwOcDcD4AwAMAAAAAAAAAwACfD+APAOAGAAAAAAAAAIACOAzgBwDgBwAAAAAAAAAAAuAYwAAAQAcAAAAAAAAAADbAOQAAAMAAAAAAAAAAAABmAHMAAADABQAAAAAAAAAAbADBAAAAAAcAAAAAAAAAANgAhwAAAAAGAAAAAAAAAACYAA4AAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAACACAAAAAAAAAAAAAAAAAABgAgAAAAAAAAAAAAAAAAAAYAYAAAAAAAAAAAAAAAAAAEAdAAAAAAAAAAAAAAAAAACANwAAAAAAAAAAAAAAAAAAgGMAAAAAAAAAAAAAAAAA4P//AwAAAAAAAAAAAAAAAOD//wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
9
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAACA/wAAAAAAAAAAAAAAAAAAAEQAAAAAAAAAAAAAAAAAAABgBgAAAAAAAAAAAAAAAAAA7BwAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAYA4AAAAAAAAAAAAAAAAABAAOABAAAAAAAAAAAAAABgMABwAwAAAAAAAAAAAABAABjAAAYAAAAAAAAAAAAAAAAMwMAMAAAAAAAAAAAAAAAAAMAAGAAAAAAAAAAAAAAAAADAADAAAAAAAAAAAAAAAMAAwAMgAAAAAAAAAAAAABx4AIB/YAAAAAAAAAAAAAAEHACA4GEAAAAAAAAAAAAAAAcAAIBjAAAAAAAAAAAAAIABAAAAQgAAAAAAAACA/wHAAAAAAAAAAAAAAAAAAMD/XwAAEAAAAAAAAAAAAAYA/AMAAhAAAAAAAAAAAIADAAAAAAYYAAAAAAAAAACAAQAAAAAMCAAAAAAAAAAA4AAAAAAADAwAAAAAAAAAAHwAAAAAABgMAAAAAAAAAAAeACAAAAAQBAAAAAAAAAAAGABgAAAAAAYAAAAAAAAAADgAYAAAAAACAAAAAAAAAAAAAEAAAAAAAwAAAAAAAAwAYADAAAAAgAcAAAAAAAAAAGAAwAAAAKAEAAAAAAAAAADAAIAAAAAgAAAAAAAAAAAA4AGAAAAAwAAAAAAAAAAAAKADgAEAA0AAAAAAAAAAAABgDwAAAAKAQAAAAAAAAAAAYB8AAAAG4EAAAAAAOAAAAMB4AAEABOEADwAAAPABAAHA8QMDAIzAAR4AAADAi4ABgIEDHwDMADwwAAAAwMDAAAADA///D3z8YAAAAIB5wAAAAwbi//vxuQMAAAAAH2AAAAMMAgDAf2AOAAAAAAAwAAADGwYAAPhjOAAAAMACHAAAgxkGAAAAR7ADAADA/w8AgIExBgAAAExgAwAAAP8HAIDBMAQAAAAMwAYAAAA8AADAcSAMAAAABIAMAAAAAAAA8DmwDAAAABaAGQAAAAAAAPAYsBkAAAAbgBkAAAAAAADwDKA3AACAG4ARAAAAAAAA4AzgcAAAxA2AOwAAAAAAAGAMgOMAgPUGAH8AAAAAAABgBgDPAeAfBgDsAAAAAAAAwAQAnAfwDQMAiAAAAAAAAMAEAHAOsI0BAPwBAAAAAADADADgHPC/AQCcAQAAAAAAgAwAwDkAMAAAOAMAAAAAAIANAIAzAAAAAPADAAAAAACAGQAAdwAAAACAAgAAAAAAgBkAAOwAAAAAAAAAAAAAAIA5AADuAQAAAAAAAAAAAACAOwAAvgMAAAAAAAAAAAAAwD8AAPwHAAAAAAAAAAAAAAA/AAD4BwAAAAAAAAAAAAAAfQAAAAAAAAAAAAAAAAAAAO4AAAAAAAAAAAAAAAAAAACEAQAAAAAAAAAAAAAAAAAA/AMAAAAAAAAAAAAAAAAAAPgDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
10
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAwMwAAAAAAAAAAAAAAAAAAPjNAQAAAAAAAAAAAAAAAAAcAAMAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAYAAADAAAAAAAAAAAAABAhjgACB4AAAAAAAAAAAAAcBAcAAwwAAAAAAAAAAAAAAAABwAMbAAAAAAAAAAAAAAA4AMADIAAAAAAAAAAAAAAAHgAAAyAAQAAAAAAAAAAAAAOAAAcAAMAAAAAAAAAAACAAwAA/gECAAAAAAAA8B8A4AEBAOQHAgAAAAAAAP7/D34AAwAAHgIAAAAAAIAPAA8PAAYAADgCAAAAAADAAQAAAAAEAAAQAgAAAAAAcAAAAAAAECAAAAAAAAAAAD4AAAAAABAwAAAAAAAAAIAfAAAAAAAAGAAAAAAAAACADwAAAAAAAAwAAAAAAAAAgA8AAAAAAAAMAAAAAAAAAAAEAAAAAAAABgAAAAAAAAAADgAAAAAAgAMAAAAAAAAAABoAAAAAAIACAAAAAAAAAAAbABAAAACAAgAAAAAAAAAAGwAQAAAAwAIAAAAAAAAAADEAEAAAAMACAAAAAAAAAIAxABAAAAAAAgAAAAAAAACAYQAYAAAM4AIAAAAAAAAQgOEAGAAAAMAHAAAAAAAACIDBAQgAAAAGBwAAAAAAAAyAAAd4AAAcAgwAAAAA4AAEwAAE/AMAPAL8AAAAAMADBuAABow/gH/44AAAAAAAA0NwwAUE8P/nTwABAAAAAABhPOAEBgAAgP8fBgAAAAAMYB5wBgMAAADAPwYAAAAACLgfOAYDAAAAgHMAAAAAADjPBx6GAQAAAADnAAAAAADnR0EHwwAAAAAAjAQAAAAA/mDxgeMAAAAAAJwEAAAAAACgeMBhAAAAAACYDQAAAAAAMB7gMQAAAAAA8A0AAAAAABgHYBEAAAAAAOANAAAAAACYA+ARAAAAAACACQAAAAAA4AHAEQAAAAAAADkAAAAAAHcAgAEAAAAAAAD4AAAAAPA7AIAzAAAAAACAiQEAAAD0HAAANgAAAAAAgAkDAAAAHw4AAGYAAAAAAMAPHgAAgAkHAADsAAAAAADA/XwAAMCgAwAA3AAAAAAA4P/5AQBg/AEAAJgBAAAAAGCHjwMAcHwAAACYAwAAAABABjcAAPAHAAAAMAcAAAAAwAIwAADAAAAAAHAGAAAAAIADAAAAAAAAAABwDgAAAACAAQAAAAAAAAAAwB8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAYwAAAAAAAAAAAAAAAAAAgP8AAAAAAAAAAAAAAAAAAAD+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
11
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwH8AAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAAAAAA8H8DAAAAAAAAAAAAAAAAAH4ABwAAAAAAAAAAAAAAAIAPAAwAAAAAAAAAAAAAAATgAQAYAAAAAAAAAAAAAAAAPAAAMAAAAAAAAAAAAAAAAA8AIHgAAAAAAAAAAAAAAMADACDYAAAAAAAAAAAAAAD+AAAwkAEAAAAAAAAAAACADwAAOBADAAAAAAAAAAAAwAMAADgAAwAAAAAAAPwfAPABAABwAAYAAAAAAID/////AAAA/AMMAAAAAADAB4A/AAAAAAEPAAAAAAAA4AEAAAAADMABOAwAAAAAAH8AAAAAAADgAHAMAAAAAOAfAAAAAABgMABADAAAAIC7DwAAAAAAQBgAAAAAAADgzQ8AAAAAAMAcAAAAAAAA8AAOAAAAAACADwAAAAAAAHgADgAAAAAAgAcAAAAAAAA4AA4AAAAAAAADAAAAAAAACAAOAAAAAACAAQAAAAAAAAAADgAIAAAAgAEAAAAAAAAAAB4ADAAACIABAAAAAAAAAAAeAAwAAAiAAQAAAAAAAAAAvgAMAAAIgAEAAAAAAAAAAPYBBgAACIABAAAAAAAAAABmA38AADiIAQAAAAAAAAAAYoIPAAA/DAMAAAAAAAAAAGKA/wAAeAQGAAAAAAAgAABgwPADAGAEDAAAAAAAIwBgYMCA//PPBzgAAAAAQAYAOHBgAPD/w5/hAAAAAMAEAB4wcAAAAAD7gAEAAADAAAADODAAAAAA5wcDAAAAgAMABx0cAAAAAIY/BwAAAAB/gO8fDgAAAAAscQwAAAAAfsD3DwcAAAAAOOMAAAAAAADA+IMBAAAAADDCAwAAAAAAYN7AAAAAAABgBgMAAAAAAADHYAAAAAAAwAw2AAAAAAAww3AAAAAAAMANbgAAAAAAmMExAAAAAACADewAAAAA/syAEQAAAAAAgA/YAAAAAI9kgBkAAAAAAAAXmAEAAACHMwAbAAAAAAAAPrgDAAAAjjEAEwAAAAAAAHz4AwAAAJgYABIAAAAAAAD88AcAAADgHwA2AAAAAAAA+MEMAAAAwAcANgAAAAAAAPBDDAAAAAACAGwAAAAAAAAwwggAAAAAAADMAAAAAAAAYIAfAAAAAAAAiAEAAAAAAOAcFAAAAAAAAIwBAAAAAADAewAAAAAAAAC+AwAAAAAAgPcAAAAAAAAAPA8AAAAAAADGAQAAAAAAAEAdAAAAAAAA/gMAAAAAAADgPwAAAAAAAPwAAAAAAAAA4D8AAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
12
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwH8AAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAAAAAA8H8DAAAAAAAAAAAAAAAAAH4ABwAAAAAAAAAAAAAAAIAPAAwAAAAAAAAAAAAAAATgAQAYAAAAAAAAAAAAAAAAPAAAMAAAAAAAAAAAAAAAAA8AIHgAAAAAAAAAAAAAAMADACDYAAAAAAAAAAAAAAD+AAAwkAEAAAAAAAAAAACADwAAOBADAAAAAAAAAAAAwAMAADgAAwAAAAAAAPwfAPABAABwAAYAAAAAAID/////AAAA/AMMAAAAAADAB4A/AAAAAAEPAAAAAAAA4AEAAAAADMABOAwAAAAAAH8AAAAAAADgAHAMAAAAAOAfAAAAAABgMABADAAAAIC7DwAAAAAAQBgAAAAAAADgzQ8AAAAAAMAcAAAAAAAA8AAOAAAAAACADwAAAAAAAHgADgAAAAAAgAcAAAAAAAA4AA4AAAAAAAADAAAAAAAACAAOAAAAAACAAQAAAAAAAAAADgAIAAAAgAEAAAAAAAAAAB4ADAAACIABAAAAAAAAAAAeAAwAAAiAAQAAAAAAAAAAvgAMAAAIgAEAAAAAAAAAAPYBBgAACIABAAAAAAAAAABmA38AADiIAQAAAAAAAAAAYoIPAAA/DAMAAAAAAAAAAGKA/wAAeAQGAAAAAAAgAABgwPADAGAEDAAAAAAAIwBgYMCA//PPBzgAAAAAQAYAOHBgAPD/w5/hAAAAAMAEAB4wcAAAAAD7gAEAAADAAAADODAAAAAA5wcDAAAAgAMABx0cAAAAAIY/BwAAAAB/gO8fDgAAAAAscQwAAAAAfsD3DwcAAAAAOOMAAAAAAADA+IMBAAAAADDCAwAAAAAAYN7AAAAAAABgBgMAAAAAAADHYAAAAAAAwAw2AAAAAAAww3AAAAAAAMAMbgAAAAAAmMExAAAAAACADewAAAAA/syAEQAAAAAAgA/YAAAAAI9kgBkAAAAAAAAXmAEAAACHMwAbAAAAAAAAPrgDAAAAjjEAEwAAAAAAAHz4AwAAAJgYABIAAAAAAAD88AcAAADgHwA2AAAAAAAA+MEMAAAAwAcANgAAAAAAAPBDDAAAAAACAGwAAAAAAAAwwggAAAAAAADMAAAAAAAAYIAfAAAAAAAAiAEAAAAAAOAcFAAAAAAAAIwBAAAAAADAewAAAAAAAAC+AwAAAAAAgPcAAAAAAAAAPA8AAAAAAADGAQAAAAAAAEAdAAAAAAAA/gMAAAAAAADgPwAAAAAAAPwAAAAAAAAA4D8AAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
13
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+PgAAAAAAAAAAAAAAAAAA4DwAAAAAAAAAAAAAAAAAAMAtAAAAAAAAAAAAAAAAABj43wAAAAAAAAAAAAAAAAAMEMABAAAAAAAAAAAAAAAAwAGAAwAAAAAAAAAAAAAAwGAAAAcAAAAAAAAAAAAAAHA4AAAMAAAAAAAAAAAAAMAgDgAMDwAAAAAAAAAAAADwBwYADB8AAAAAAAAAAAAA8IcAAA42AAAAAAAAAAAAAAB8AAAOZAAAAAAAAAAAAAAADwAADmAAAAAAAAAA/AcAgAMAAA7AAAAAAAAAgP/+APABAAB/gAEAAAAAAPAB4P8/AACA4YEBAAAAAAA4AAjgAQAAAAAHAQAAAAAAHgAAAAAAAAAAjgEAAAAAgAsAAAAAAAAAAIwBAAAAAOAEAAAAAAAAGACIAQAAAAAABAAAAAAAQAwAAAAAAAAACAQAAAAAAMAMAAAAAAAAAA4GAAQAAACABgAAAAAAAAADBAAEAAAAgAMAAAAAAACAAQQABAAAAIABAAAAAAAAAAAAAAQAAAAAAQAAAAAAAAAAAAAGAAAAAAMAAAAAAACAAAAABgAAAAADAAAAAAAAEAAAAAMAAAAAAwAAAAAAABCAF4ADAAAAAAMAAAAAAAAA8A+AHwAAAIADAAAAAAAACBAM4B8AAADAAwAAAAAAAAwGDvA5AAAA4AIAAAAAAAAGGgc4cAAAOGAGAAAAAAAABpwDHMAHAPAADAAAAAAAAAf9AA4A/wzcARgAAAAAAICDv4AHAPD/HwcwAAAAAADDgz/gAQAADAyUYAAAAACg4cEHeAAAAACE8cMBAAAA8IDBDw4AAAAAhMCPAwAAAODP4AsBAAAAAMQABwcAAAAARnPDAQAAAABEABwOAAAAAIB/wwAAAAAAZABwDAAAAADAvAEAAAAAAGYA4AgAAAAA4I0ZAAAAAABmAIBjAAAAAHDQDAAAAAAAZgAAxwMAAAD4/wYAAAAAADYAANgPAAAA/HcDAAAAAAA2AABwHAAAAAC8AwAAAAAAHgAAwPEAAADAnAEAAAAAAB4AAIDvAwAA8McAAAAAAAAPAAAAjw8AALjjAAAAAAAADwAAAAgAAAAcfwAAAAAAgA8AAAD4AQAALD8AAAAAAIALAAAA8AEAAP4/AAAAAACADwAAAAAAAAB4AAAAAAAAwAwAAAAAAAAAAAAAAAAAAMAMAAAAAAAAAAAAAAAAAADADAAAAAAAAAAAAAAAAAAAgA8AAAAAAAAAAAAAAAAAAAA7AAAAAAAAAAAAAAAAAAAAcwAAAAAAAAAAAAAAAAAAAOMAAAAAAAAAAAAAAAAAAAD/AQAAAAAAAAAAAAAAAAAA/wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
14
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAADAHAAAAAAAAAAAAAAAAAAA4A8AAAAAAAAAAAAAAAAAIPhvAAAAAAAAAAAAAAAAAPAcwAAAAAAAAAAAAAAAAACAAIABAAAAAAAAAAAAAAAAAgAAAwAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAMAIYPAAAAAAAAAAAAAABwBgCHCwAAAAAAAAAAAAAAAAMAAxgAAAAAAAAAAAAAQIADAAcTAAAAAAAAAAAAAAD+AAAHIAAAAAAAAADwAQAAHgAAD2AAAAAAAAAA/x8AgAcAgHvAAAAAAAAAwAf4B8ABAIDhgQAAAAAAAHAAAOdvAAQAgIMAAAAAAIAfAAAAAAAMAADGAAAAAADwAwAAAAAAGAAAxAAAAAAAeAEAAAAAABgQAMQAAAAAAOAFAAAAAAAwGAAAAAAAACwgDAAAAAAAIAwAAAAAAAABAAQAAAAAAEAMAAAAAAAAAAAEAAAAAAAABgAAAAAAAAAADAAAAAAAAAYAAAAAAAAAAAwABAAAAIADAAAAAAAAQAAMAAAAAACAAQAAAAAAAAAADAAAAAAAgAEAAAAAAAAwAAwAAQAAAIABAAAAAAAAGOAPgAEAAACAAQAAAAAAYAzwDYABAAAAgAEAAAAAACAE8AbAHwAAAMAAAAAAAAAwBJADwAcAAADAAAAAAAAAMwH+ATAfAAAAQAAAAAAAABMBfgAA+AAAAAAAAAAAAACbAR8AAOAHAACIAAAAAAAA24EDwAEA/wNgjAAAAAAAAOuAAf4AAPD//4ABAAAAAIDvgMAfAAAA4PkDAQAAAADA98DgAAAAAGCYFwMAAAAA4HpgeAAAAABgDDwDAAAAAMA/YE4AAAAAcAd4BgAAAACADzBHAAAAAHAD+AYAAAAAAACYZwAAAAA4A+AMAAAAAAAAzmcAAAAAuAEAHAAAAAAAMMM7AAAAAIwAABgAAAAAAPjjPgAAAADMAIAxAAAAAAD8sQ4AAAAAbAAAMQAAAAAAPvwHAAAAADYAACYAAAAAAPf/AAAAAAAbAAAcAAAAAID/NwAAAAAACwAAOAAAAACADwAAAAAAgA0AAHAAAAAAAAAAAAAAAMAGAADgBgAAAAAAAAAAAABgBgAAwBwAAAAAAAAAAAAAcAMAAAA4AAAAAAAAAAAAADADAAAA8AAAAAAAAAAAAAAcAQAAAOMHAAAAAAAAAAAA+AEAAAA+PgAAAAAAAAAAALABAAAA+BAAAAAAAAAAAAAgAwAAAOAEAAAAAAAAAAAAYA8AAADABwAAAAAAAAAAAOAPAAAAAAAAAAAAAAAAAADgHQAAAAAAAAAAAAAAAAAA4D8AAAAAAAAAAAAAAAAAAOA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
15
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAMAAAAAAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAAAAC8HwAAAAAAAAAAAAAAAAAAxDcAAAAAAAAAAAAAAAAAAAxwAAAAAAAAAAAAAAAAAOAPwAAAAAAAAAAAAAAAAADfA4ABAAAAAAAAAAAAAAAA/wHBAwAAAAAAAAAAAAAAAP4A4QcAAAAAAAAAAAAAAAB8gMEGAAAAAAAAAAAAAAAAHoCBDQAAAAAAAAAAAAAAFA+AARgAAAAAAAAAAAAAAJwPgAMQAAAAAAAAAAAAAAD4B4APcAAAAAAAAAD/HwAA/AGAfWAAAAAAAADAv/8DAB8AwOBAAAAAAAAA4ADgf8APAOCAYQAAAAAAgD8AAPD/AQBgAGMAAAAAAPgfAAAASAAIMABgAAAAAAC/AQAAAAAAGDAAAAAAAADA9wkAAAAAABgYAAAAAAAAeDwIAAAAAAAwDAAAAAAAAA4GDAAAAAAAYAwAAAAAAAAAAAwAAAAAAGAGAAAAAAAAAAAMAAQAAABgBgAAAAAAAwAADAAGAAAAQAIAAAAAAAEAAAwAAgAAAMADAAAAAIABAAA8AAIAAADAAwAAAACAAAAALAADAAAAwAEAAAAAAAAAAG4AAwAAAMAAAAAAAAAAAMAHgAEAAADAAAAAAAAAAAD8A4A/AAAAwAAAAAAAAAAA/gHAPwAAAMAAAAAAAAAAgGAA8A8AAAJgAAAAAAAAAMAAAPgfAAAHcAAAAAAAAABgAIA/fAAABzgAAAAAACAAYOD/D/AHAI4fAAAAAAAgADD8/wCA/8fsCwAAAAAAYACIDwAAAPj//wgAAAAAAGAIjAEAAAAA/zkYAAAAAABgBocABgAAAOMwGAAAAAAAPOIxAAYAAABhYBgAAAAAACDsGAAGAACAM+AZAAAAAAAAfA7ABgAAwBjAGwAAAAAAADMHgAYAAOAcwBgAAAAAAIDrA0AGAABgDoAZAAAAAACA/QF4BgAAYAaAMwAAAAAAwF8AfAYAADADACMAAAAAAMAPAEwDAAAAAQAjAAAAAAAAAADeAwAADgAAZgAAAAAAAAAA4gAAAD8AAGQAAAAAAAAAAAIAAMAfAAAMAAAAAAAAAAACAADwDgAAaAAAAAAAAAAAAAAAMAYAABgAAAAAAAAAAAAAAD4DAAAQAAAAAAAAAAAAAAD/AQAAsAAAAAAAAAAAAACA9QAAALABAAAAAAAAAAAAgD0AAABgAwAAAAAAAAAAAMAPAAAAYAMAAAAAAAAAAADAAQAAAGAGAAAAAAAAAAAAAAAAAADgDAAAAAAAAAAAAAAAAAAA4A8AAAAAAAAAAAAAAAAAAIA7AAAAAAAAAAAAAAAAAAAA+wAAAAAAAAAAAAAAAAAAAN8BAAAAAAAAAAAAAAAAAAD+AwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
16
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAADAAQAAAAAAAAAAAAAAAAAAfAcAAAAAAAAAAAAAAAAAAPwPAAAAAAAAAAAAAAAAAAAeGQAAAAAAAAAAAAAAAADgADAAAAAAAAAAAAAAAAAA8ABgAAAAAAAAAAAAAAAAAPAAwAAAAAAAAAAAAAAAAADwgOABAAAAAAAAAAAAAAAAfcDwAwAAAAAAAAAAAAAAAD/AYAcAAAAAAAAAAAAAAAAewMAGAAAAAAAAAAAAAADAAcAACAAAAAAAAAAAAAAA+AXAARgAAAAAAAAAAAAAAH4AwA8wAAAAAAAAAPgfAAAeAMA+IAAAAAAAAAA/OACABwBg4DAAAAAAAACAAwD/5wAAAMAxAAAAAAAAAAAY/G8AAACAMQAAAAAAAAwAEAAAAAAAABAAAAAAAMAPAAAAAAAAAAAAAAAAAADgAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAACAAAAAAAAAQAAAAAAAAAPggAAAAAACAGAAAAAIAHAAwIAAAAAAAAAgAAAADAAYADGAAAAAAAQAMAAAAA4ADwARgADAAAAAADAAAAAGAAeAAYAAwAAACAAQAAAAAwCAgAEAAMAAAAwAAAAAAAEAwAADAABgAAgAAAAAAAABAAAAA4AAYAAAAAAAAAAADQAgAAHAAPAAAAAAAAAAAAUAIAAA8APwAAADAAAAAAADACAP4AwD8AgAEwAAAAAAAQAAACAPB4AIABHAAAAAAAGAEAAAA+4AeAAwwAAAAAABgGAAPwH4D/AGMCAAAAAAB4AwAD/wEA8D9/AwAAAAAAvAMA4zEAAACABwMAAAAAAP4BAHMkAAAAgIcBAAAAAAB+AAAZbAAAAICGAAAAAAAAAAAAHWwAAADAxwAAAAAAAAAAgA1sAAAAwMcAAAAAAAAAAIAOzAAAAOBGAAAAAAAAAAAgBswAAABwZgAAAAAAAAAAOAfIAAAAGG8AAAAAAAAAAB8DiAEA+IdnAAAAAAAAAACPA5gBgPzDYwAAAAAAAABAhgGwAcAP/DMAAAAAAAAAQP8AsAHwxg8xAAAAAAAAAGB/ALgB+P8AGAAAAAAAAABgBwC4AVwbABkAAAAAAAAA8AMA+AF8AAABAAAAAAAAAPABAJABHACAAQAAAAAAAABAAACwAAAAgAUAAAAAAAAAAAAA8AAAAIAEAAAAAAAAAAAAAOAAAADABgAAAAAAAAAAAADAAAAAwAYAAAAAAAAAAAAAAAAAAGAGAAAAAAAAAAAAAAAAAAAwBgAAAAAAAAAAAAAAAAAAMAMAAAAAAAAAAAAAAAAAAOAHAAAAAAAAAAAAAAAAAADADQAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIBzAAAAAAAAAAAAAAAAAACA/gAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
17
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAPwBAAAAAAAAAAAAAAAAAAD8AwAAAAAAAAAAAAAAAAAA+A8AAAAAAAAAAAAAAAAAIIAcAAAAAAAAAAAAAAAAAKgBOAAAAAAAAAAAAAAAAACsAHAAAAAAAAAAAAAAAAAApgBgAAAAAAAAAAAAAAAAAP7A8AAAAAAAAAAAAAAAAMD8wPABAAAAAAAAAAAAAABAGGAgAwAAAAAAAAAAAAAAAABgYAMAAAAAAAAAAAAAAAAC4AAGAAAAAAAAAAAAAAAAAuAADAAAAAAAAAAAAAAATwPgBxgAAAAAAAAA4A8AwH8DYB4YAAAAAAAAAPx/AMD/AQBwCAAAAAAAAAAP4P//cQAA4BgAAAAAAACAAwD/PwAAEMAYAAAAAAAA4AAAwAAAAhiACAAAAAAAAHAAAAAAAAYIAAAAAAAAAAAeAAAAAAAECAAAAAAAAADgBwAAAAAADAwAAAAAAAD8+QcAAAAAAAgEAAAAAADg/8EHAAAAAAAIBAAAAAAAfACAGAAAAAAAAAYAAAAAgH8AcBgAAAAAAAACAAAAAMABAD8YABAAAAAAAwAAAAAAAAADEAAQAAAAAAEAAAAAAAAAATAAGAAAAIABAAAAAAAAAAEwABgAAADAAAAAAAAACHAAMAAYAAAA4AAAAAAAAA8/AGAAGAAAAGAAAAAAAMAAAwDgAAwAQAAwAAAAAABgAAAA8AB8AEAAMAAAAAAAMAAAADAAfABAADgAAAAAABgAAAAcAP4AQAAcAAAAAAAMDgDwA8DcBwAABgAAAAAADAcA+AHwD3/gCAMAAAAAAAwDABgAvwP4f44BAAAAAADGAQAA4OMBAOABAAAAAAAAYwAACGZ+AADg4AAAAAAAADkAAAifDwAAYPAAAAAAAAAIAAAIsAMAAOD4AAAAAAAAAAAAiDAGAADw3AAAAAAAAAAAAMhgBgAAcM4AAAAAAAAAAADIwAwAAHjPAAAAAAAAAAAAzIAJAACYzwAAAAAAAAAAAEyAGQAAzuwAAAAAAAAAAABMABMAeMbMAAAAAAAAAAAATAAjAB7nZwAAAAAAAAAAAGQAZwAH42MAAAAAAAAAAIBnAGYAjzk+AAAAAAAAAACAYwBsAJj9HwAAAAAAAAAAgDAAbACACwAAAAAAAAAAAIAdAG4AkAEAAAAAAAAAAAAgDgBOANgBAAAAAAAAAAAAIAcAXgDsAAAAAAAAAAAAALABAEwAYgAAAAAAAAAAAAAwAABMADMAAAAAAAAAAAAAAAAAQIAxAAAAAAAAAAAAAAAAAECAGQAAAAAAAAAAAAAAAAAAgA0AAAAAAAAAAAAAAAAAAIANAAAAAAAAAAAAAAAAAAAAGwAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAIBjAAAAAAAAAAAAAAAAAAAAwQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
18
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAA+N8BAAAAAAAAAAAAAAAAAMwBBwAAAAAAAAAAAAAAAABmAA4AAAAAAAAAAAAAAAAAPgAYAAAAAAAAAAAAAAAAAB4QPgAAAAAAAAAAAAAAAMAOGG4AAAAAAAAAAAAAAABgBxjIAAAAAAAAAAAAAAAA4AMYCAAAAAAAAAAAAAAAAPABGAADAAAAAAAAAAAAAADwATAABgAAAAAAAAAAAAAA8AHwAQQAAAAAAAAAAAAAAOAB8A8EAAAAAAAAAAAAAAD2Axg4BAAAAAAAAAAADAAA/wAIcAQAAAAAAAAAwP8AwH8ADOAEAAAAAAAAAPD/f/9/AAzAAQAAAAAAAAA8AP7/fwAMAAAAAAAAAAAADwAAwHcABAAAAAAAAAAAgAMAAAAwAAQAAAAAAAAAAMABAAAAAAAEAAAAAAAAAADgAAAAAAAABgAAAAAAAAAAcAAAAAAAAAYAAAAAAAAAADgAIAAAAAAGAAAAAAAAAAA+ACAAAAAAAgAAAAAAgAPANwBgAAAAGAMAAAAAAP///zEAYAAAADADAAAAAMAP/P83AGAAAACwAQAAAADwAADgIwFAAAAAoAEAAAAAGAAAAGABQAAAAOAAAAAAAAAAAABgBsAAAADgAAAAAAAAAAAAwAzAAIAA4AAAAAAAAAAAAMAZQADAAPAAAAAAAAAEAAAAB2AAwAD4AAAAAADABQAAAB7wAMAAeAAAAABAYCAAAAAe8AHAAHgAAAAAQBwwAAAABvgfwPg/AAAAAEAWAAAAAAMe/v9/fwAAAABABgAAAIADB+P/l2MAAAAAwAYAAADwgQ8DgIFhAAAAAMAGAAAAfOAMA4DBwAAAAACAAQAAAAY44A+AwsMBAAAAAAEAAAAGHvAfgGPHAQAAAAAAAAAADAfwe4BzPgMAAAAAAAAAABgDzPuAM3gGAAAAAAAAAAAwAc7PgRvgBAAAAAAAAAAAcALPnIMIwAQAAAAAAAAAAGAG/zjPDOAAAAAAAAAAAADADh/x/QxwAAAAAAAAAAAAgB0ewycGvAMAAAAAAAAAAIA7PAcPB84BAAAAAAAAAAAAc/AO/ON3AAAAAAAAAAAAAGfAc/D8PwAAAAAAAAAAAABugMcAegwAAAAAAAAAAAAAbADPAD8HAAAAAAAAAAAAAG4AnoHpAwAAAAAAAAAAAABmAHzH/wEAAAAAAAAAAAAAcADw5h8AAAAAAAAAAAAAAHYA4I0AAAAAAAAAAAAAAABsAGAMAAAAAAAAAAAAAAAAfADAHwAAAAAAAAAAAAAAAHgAgB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
19
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAA+N8BAAAAAAAAAAAAAAAAAMwBBwAAAAAAAAAAAAAAAABmAA4AAAAAAAAAAAAAAAAAPgAYAAAAAAAAAAAAAAAAAB4QPgAAAAAAAAAAAAAAAMAOGG4AAAAAAAAAAAAAAABgBxhIAAAAAAAAAAAAAAAA4AMYCAAAAAAAAAAAAAAAAPABGAADAAAAAAAAAAAAAADwATAABgAAAAAAAAAAAAAA8AHwAQQAAAAAAAAAAAAAAOAB8A8EAAAAAAAAAAAAAAD2Axg4BAAAAAAAAAAADAAA/wAIcAQAAAAAAAAAwP8AwH8ADOAEAAAAAAAAAPD/f/9/AAzAAwAAAAAAAAA8AP7/fwAMAAAAAAAAAAAADwAAwHcABAAAAAAAAAAAgAMAAAAwAAQAAAAAAAAAAMABAAAAAAAEAAAAAAAAAADgAAAAAAAABgAAAAAAAAAAcAAAAAAAAAYAAAAAAAAAADgAMAAAAAAGAAAAAAAAAAA+ACAAAAAAAgAAAAAAgAPANwBgAAAAGAMAAAAAAP///zEAYAAAADADAAAAAMAP/P83AGAAAACwAQAAAADwAADgIwBAAAAAoAEAAAAAGAAAAGAAQAAAAOAAAAAAAAAAAABgBsAAAADgAAAAAAAAAAAAwAzAAIAA4AAAAAAAAAAAAMAZQADAAPAAAAAAAAAEAAAAB2AAwAD4AAAAAADABwAAAB7wAMAAeAAAAABAYAAAAAAe8AHAAHgAAAAAQBwQAAAABvgfwPg/AAAAAEAUAAAAAAMe/v9/fwAAAABABgAAAIADB+P/l2EAAAAAwAYAAADwgQ8DgIFhAAAAAMAGAAAAfOAMA4DBwAAAAACAAQAAAAY44A+AwsMBAAAAAAEAAAAGHvAfgGPHAQAAAAAAAAAADAf0e4BzPgMAAAAAAAAAABgDzPuAM3gGAAAAAAAAAAAwAc7PgRvgBAAAAAAAAAAAcALPnIMIwAQAAAAAAAAAAGAGzzjPDOAAAAAAAAAAAADADp/x/QxwAAAAAAAAAAAAgB2ewycGnAMAAAAAAAAAAIA7PAcOB84BAAAAAAAAAAAAc/Ae/ON3AAAAAAAAAAAAAGfAc/D8PwAAAAAAAAAAAABugMcAegwAAAAAAAAAAAAAbADPAD8HAAAAAAAAAAAAAG4AnoHpAwAAAAAAAAAAAABmAHzH/wEAAAAAAAAAAAAAcADw5h8AAAAAAAAAAAAAAHYA4I0AAAAAAAAAAAAAAABsAGAMAAAAAAAAAAAAAAAAfADAHwAAAAAAAAAAAAAAAHgAgB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
20
+ ];
@@ -8,6 +8,7 @@ export const receiverStyleRegistry = [
8
8
  { id: 'cyclone', maxRows: 14, minRows: 7 },
9
9
  { id: 'nebula', maxRows: 14, minRows: 7 },
10
10
  { id: 'lava-lamp', maxRows: 14, minRows: 7 },
11
+ { id: 'gallop-fine', maxRows: 18, minRows: 7 },
11
12
  { id: 'motion-contour', maxRows: 14, minRows: 8 },
12
13
  { id: 'ribbon', maxRows: 12, minRows: 7 },
13
14
  { id: 'braille-wave', maxRows: 12, minRows: 7 },
@@ -60,12 +61,21 @@ export const receiverStyleRegistry = [
60
61
  { id: 'skyline', maxRows: 14, minRows: 7 },
61
62
  { id: 'golden-gate', maxRows: 14, minRows: 7 },
62
63
  { id: 'manhattan', maxRows: 14, minRows: 7 },
63
- { id: 'alpine', maxRows: 14, minRows: 7 }
64
+ { id: 'alpine', maxRows: 14, minRows: 7 },
65
+ { id: 'crossfade', maxRows: 14, minRows: 7 },
64
66
  ];
65
67
  export const receiverStyleNames = receiverStyleRegistry.map(style => style.id);
66
- export const defaultReceiverStyle = 'ultracode';
68
+ export const defaultReceiverStyle = 'gallop-fine';
67
69
  export const receiverStyleMetadata = Object.fromEntries(receiverStyleRegistry.map(style => [style.id, style]));
68
70
  const retiredReceiverStyleNames = [
71
+ 'duplex-wave', 'color-drift', 'undertow', 'cube-tide', 'parallax-slabs', 'voxel-swell',
72
+ 'dot-current', 'flip-matrix', 'raster-harp',
73
+ 'spin-lattice', 'packet-storm', 'spark-chamber',
74
+ 'magnetic-bloom', 'liquid-chrome', 'vector-braid', 'halftone-orbit',
75
+ 'sonic-fan', 'raster-disc', 'diamond-current', 'velvet-vortex',
76
+ 'din-stack', 'bass-shutters', 'velvet-dither', 'chrome-sweep', 'prism-steps', 'stereo-iris',
77
+ 'phosphor-marquee', 'ascii-drive', 'prism-bank', 'gallop', 'gallop-ultrafine',
78
+ 'gallop-solid', 'gallop-shaded', 'gallop-dither', 'gallop-hatch', 'gallop-scanline',
69
79
  'scope', 'spectrum', 'oscilloscope', 'sdr', 'signal', 'retro', 'neon', 'waterfall',
70
80
  'cassette', 'casette', 'stars', 'radio-waves', 'raindrops', 'vinyl', 'soundwave',
71
81
  'spectrum-3d', 'tuning-dial', 'rf-constellation', 'rf-constelation', 'sphere', 'mobius',