@ciphore/radiocli 0.1.7 → 0.1.9
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 +42 -0
- package/README.md +2 -2
- package/dist/activity/stats.js +4 -3
- package/dist/storage/store.js +7 -5
- package/dist/types.js +1 -1
- package/dist/ui/App.js +73 -11
- package/dist/ui/AppContent.js +2 -2
- package/dist/ui/app-state.js +21 -0
- package/dist/ui/components/Logo.js +2 -1
- package/dist/ui/components/Menu.js +2 -1
- package/dist/ui/components/ScreenHeader.js +2 -2
- package/dist/ui/components/StationList.js +4 -4
- package/dist/ui/components/TopTabs.js +3 -3
- package/dist/ui/layout.js +2 -1
- package/dist/ui/page-footer.js +5 -4
- package/dist/ui/screens/AirPlayCodeScreen.js +3 -3
- package/dist/ui/screens/AirPlaySettingsScreen.js +4 -4
- package/dist/ui/screens/CountriesScreen.js +8 -2
- package/dist/ui/screens/ExploreScreen.js +2 -2
- package/dist/ui/screens/HelpScreen.js +2 -2
- package/dist/ui/screens/HomeScreen.js +2 -2
- package/dist/ui/screens/MapScreen.js +3 -3
- package/dist/ui/screens/NowPlayingScreen.js +5 -5
- package/dist/ui/screens/SearchScreen.js +2 -2
- package/dist/ui/screens/SettingsScreen.js +3 -3
- package/dist/ui/screens/StationScreen.js +2 -1
- package/dist/ui/screens/StatsScreen.js +115 -29
- package/dist/ui/screens/screen-render.test.js +65 -3
- package/dist/ui/theme.js +6 -3
- package/dist/ui/use-app-input.js +6 -2
- package/dist/ui/visualizers/receiver-visualizers.js +94 -75
- package/package.json +1 -1
|
@@ -3,8 +3,8 @@ export function buildVisualizer(style, pulse, width, height, _station, playback,
|
|
|
3
3
|
if (!playbackHasSignal(playback)) {
|
|
4
4
|
return buildZeroSignalVisualizer(width, height, theme);
|
|
5
5
|
}
|
|
6
|
-
if (style === '
|
|
7
|
-
return
|
|
6
|
+
if (style === 'ultracode') {
|
|
7
|
+
return buildUltracode(pulse, width, height, theme);
|
|
8
8
|
}
|
|
9
9
|
if (style === 'motion-blob') {
|
|
10
10
|
return buildMotionBlob(pulse, width, height, theme);
|
|
@@ -153,7 +153,7 @@ export function buildVisualizer(style, pulse, width, height, _station, playback,
|
|
|
153
153
|
if (style === 'newton') {
|
|
154
154
|
return buildNewton(pulse, width, height, theme);
|
|
155
155
|
}
|
|
156
|
-
return
|
|
156
|
+
return buildUltracode(pulse, width, height, theme);
|
|
157
157
|
}
|
|
158
158
|
function playbackHasSignal(playback) {
|
|
159
159
|
return playback.state === 'playing' && playback.ready;
|
|
@@ -172,71 +172,80 @@ function buildFlatZeroSignal(width, requestedHeight, theme) {
|
|
|
172
172
|
color: rowIndex === height - 1 ? accent : '#767676'
|
|
173
173
|
}));
|
|
174
174
|
}
|
|
175
|
-
|
|
175
|
+
const ultracodeWavelength = 20;
|
|
176
|
+
const ultracodeTravelPerPulse = 80 * 0.03;
|
|
177
|
+
const ultracodeVioletRamp = ['#3e1676', '#491e87', '#542799', '#5f2faa', '#6b37bc', '#763fcd', '#8148df', '#8c50f0'];
|
|
178
|
+
function buildUltracode(pulse, width, height, theme) {
|
|
179
|
+
const renderWidth = Math.max(1, width);
|
|
180
|
+
const renderHeight = Math.max(1, height);
|
|
176
181
|
const accent = themeAccent(theme);
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const p = (pulse - k + 240) % 240;
|
|
191
|
-
const low = Math.sin(i * 0.18 + p * 0.46);
|
|
192
|
-
const mid = Math.cos(i * 0.32 - p * 0.28);
|
|
193
|
-
const high = Math.sin(i * 0.45 + p * 0.68);
|
|
194
|
-
const normalized = (low * 0.4 + mid * 0.35 + high * 0.25 + 1) / 2;
|
|
195
|
-
const eased = Math.pow(Math.max(0, Math.min(1, normalized)), 1.4);
|
|
196
|
-
const lvl = Math.round(eased * height);
|
|
197
|
-
if (lvl > maxLvl) {
|
|
198
|
-
maxLvl = lvl;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return maxLvl;
|
|
202
|
-
});
|
|
203
|
-
const rows = [];
|
|
204
|
-
const colors = themeContributionColors(theme);
|
|
205
|
-
for (let rowIndex = 0; rowIndex < height; rowIndex++) {
|
|
206
|
-
const threshold = height - rowIndex;
|
|
207
|
-
let rowText = ' ';
|
|
208
|
-
for (let i = 0; i < bandCount; i++) {
|
|
209
|
-
const lvl = levels[i];
|
|
210
|
-
const peak = peaks[i];
|
|
211
|
-
let bandChar = ' ';
|
|
212
|
-
if (lvl >= threshold) {
|
|
213
|
-
bandChar = '██';
|
|
214
|
-
}
|
|
215
|
-
else if (peak === threshold) {
|
|
216
|
-
bandChar = '◆◆';
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
bandChar = '··';
|
|
220
|
-
}
|
|
221
|
-
rowText += bandChar + ' ';
|
|
222
|
-
}
|
|
223
|
-
let color = colors[2] ?? accent;
|
|
224
|
-
if (rowIndex < Math.max(1, height * 0.3)) {
|
|
225
|
-
color = '#ff5f87';
|
|
226
|
-
}
|
|
227
|
-
else if (rowIndex < Math.max(2, height * 0.6)) {
|
|
228
|
-
color = colors[4] ?? accent;
|
|
229
|
-
}
|
|
230
|
-
else {
|
|
231
|
-
color = colors[3] ?? accent;
|
|
232
|
-
}
|
|
233
|
-
rows.push({
|
|
234
|
-
text: rowText.padEnd(width).slice(0, width),
|
|
235
|
-
color
|
|
182
|
+
const ramp = ultracodeRippleRamp(theme);
|
|
183
|
+
const selectedColor = ramp[ramp.length - 1] ?? accent;
|
|
184
|
+
const travel = pulse * ultracodeTravelPerPulse;
|
|
185
|
+
const originColumn = Math.floor(renderWidth / 2);
|
|
186
|
+
const originRow = Math.floor(renderHeight / 2);
|
|
187
|
+
const rows = Array.from({ length: renderHeight }, (_, rowIndex) => {
|
|
188
|
+
const cells = Array.from({ length: renderWidth }, (_, columnIndex) => {
|
|
189
|
+
const level = ultracodeRippleLevel(ultracodeDistance(columnIndex, rowIndex, originColumn, originRow), travel, ramp.length);
|
|
190
|
+
return {
|
|
191
|
+
text: ' ',
|
|
192
|
+
color: level === null ? accent : selectedColor,
|
|
193
|
+
backgroundColor: level === null ? undefined : ramp[level] ?? selectedColor
|
|
194
|
+
};
|
|
236
195
|
});
|
|
237
|
-
|
|
196
|
+
return lineFromCells(cells, accent);
|
|
197
|
+
});
|
|
238
198
|
return rows;
|
|
239
199
|
}
|
|
200
|
+
function ultracodeDistance(column, row, originColumn, originRow) {
|
|
201
|
+
const dx = column - originColumn;
|
|
202
|
+
const dy = (row - originRow) * 2;
|
|
203
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
204
|
+
}
|
|
205
|
+
function ultracodeRippleLevel(distance, travel, rampLength) {
|
|
206
|
+
if (distance > travel) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const phase = (((distance - travel) % ultracodeWavelength) + ultracodeWavelength) % ultracodeWavelength;
|
|
210
|
+
const brightness = (1 + Math.cos((2 * Math.PI * phase) / ultracodeWavelength)) / 2;
|
|
211
|
+
return Math.min(rampLength - 1, Math.round(brightness * (rampLength - 1)));
|
|
212
|
+
}
|
|
213
|
+
function ultracodeRippleRamp(theme) {
|
|
214
|
+
if (theme === 'violet') {
|
|
215
|
+
return ultracodeVioletRamp;
|
|
216
|
+
}
|
|
217
|
+
const colors = themeContributionColors(theme);
|
|
218
|
+
const start = colors[1] ?? '#1c1c1c';
|
|
219
|
+
const end = themeAccent(theme);
|
|
220
|
+
return Array.from({ length: 8 }, (_, index) => interpolateHex(start, end, index / 7));
|
|
221
|
+
}
|
|
222
|
+
function interpolateHex(startHex, endHex, amount) {
|
|
223
|
+
const start = hexToRgb(startHex);
|
|
224
|
+
const end = hexToRgb(endHex);
|
|
225
|
+
return rgbToHex([
|
|
226
|
+
Math.round(start[0] + (end[0] - start[0]) * amount),
|
|
227
|
+
Math.round(start[1] + (end[1] - start[1]) * amount),
|
|
228
|
+
Math.round(start[2] + (end[2] - start[2]) * amount)
|
|
229
|
+
]);
|
|
230
|
+
}
|
|
231
|
+
function hexToRgb(hex) {
|
|
232
|
+
const normalized = hex.replace('#', '');
|
|
233
|
+
const expanded = normalized.length === 3
|
|
234
|
+
? [...normalized].map(value => value + value).join('')
|
|
235
|
+
: normalized.padEnd(6, '0').slice(0, 6);
|
|
236
|
+
const value = Number.parseInt(expanded, 16);
|
|
237
|
+
return [
|
|
238
|
+
(value >> 16) & 255,
|
|
239
|
+
(value >> 8) & 255,
|
|
240
|
+
value & 255
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
function rgbToHex([red, green, blue]) {
|
|
244
|
+
return `#${[red, green, blue].map(value => clampColor(value).toString(16).padStart(2, '0')).join('')}`;
|
|
245
|
+
}
|
|
246
|
+
function clampColor(value) {
|
|
247
|
+
return Math.max(0, Math.min(255, Math.round(value)));
|
|
248
|
+
}
|
|
240
249
|
function buildMotionBlob(pulse, width, height, theme) {
|
|
241
250
|
const h = Math.max(7, height);
|
|
242
251
|
const mid = (h - 1) / 2;
|
|
@@ -1876,11 +1885,19 @@ function lineFromCells(cells, fallbackColor) {
|
|
|
1876
1885
|
const segments = [];
|
|
1877
1886
|
for (const cell of cells) {
|
|
1878
1887
|
const previous = segments[segments.length - 1];
|
|
1879
|
-
if (previous &&
|
|
1888
|
+
if (previous &&
|
|
1889
|
+
previous.color === cell.color &&
|
|
1890
|
+
previous.backgroundColor === cell.backgroundColor &&
|
|
1891
|
+
previous.bold === cell.bold) {
|
|
1880
1892
|
previous.text += cell.text;
|
|
1881
1893
|
}
|
|
1882
1894
|
else {
|
|
1883
|
-
segments.push({
|
|
1895
|
+
segments.push({
|
|
1896
|
+
text: cell.text,
|
|
1897
|
+
color: cell.color,
|
|
1898
|
+
backgroundColor: cell.backgroundColor,
|
|
1899
|
+
bold: cell.bold
|
|
1900
|
+
});
|
|
1884
1901
|
}
|
|
1885
1902
|
}
|
|
1886
1903
|
return { text, color: fallbackColor, segments };
|
|
@@ -1938,7 +1955,7 @@ function dimMotionColorAt(position, theme) {
|
|
|
1938
1955
|
}
|
|
1939
1956
|
export function visualizerHeight(style, availableRows, width = 80) {
|
|
1940
1957
|
const maxRowsByStyle = {
|
|
1941
|
-
|
|
1958
|
+
ultracode: 14,
|
|
1942
1959
|
'motion-blob': 12,
|
|
1943
1960
|
'motion-area': 11,
|
|
1944
1961
|
'motion-contour': 14,
|
|
@@ -1990,7 +2007,7 @@ export function visualizerHeight(style, availableRows, width = 80) {
|
|
|
1990
2007
|
newton: 14
|
|
1991
2008
|
};
|
|
1992
2009
|
const spaciousStyles = new Set([
|
|
1993
|
-
'
|
|
2010
|
+
'ultracode',
|
|
1994
2011
|
'motion-blob',
|
|
1995
2012
|
'motion-area',
|
|
1996
2013
|
'motion-contour',
|
|
@@ -2041,13 +2058,15 @@ export function visualizerHeight(style, availableRows, width = 80) {
|
|
|
2041
2058
|
'lava-lamp',
|
|
2042
2059
|
'newton'
|
|
2043
2060
|
]);
|
|
2044
|
-
const minRows = style === '
|
|
2045
|
-
?
|
|
2046
|
-
:
|
|
2047
|
-
?
|
|
2048
|
-
:
|
|
2049
|
-
?
|
|
2050
|
-
:
|
|
2061
|
+
const minRows = style === 'ultracode'
|
|
2062
|
+
? 7
|
|
2063
|
+
: style === 'cube' || style === 'motion-contour' || style === 'spinning-donut'
|
|
2064
|
+
? 8
|
|
2065
|
+
: spaciousStyles.has(style)
|
|
2066
|
+
? 7
|
|
2067
|
+
: style.startsWith('motion-')
|
|
2068
|
+
? 6
|
|
2069
|
+
: 3;
|
|
2051
2070
|
const baseMaxRows = maxRowsByStyle[style] ?? 8;
|
|
2052
2071
|
const maxRows = responsiveVisualizerMaxRows(style, availableRows, width, baseMaxRows, spaciousStyles);
|
|
2053
2072
|
return Math.max(minRows, Math.min(maxRows, availableRows));
|