Skip to content

Commit 9bce2e4

Browse files
committed
feat: AOS countdown in selection window, deduplicate time formatters
Auto-compute passes when satellites are selected (no longer gated on passes window visibility) and show a live AOS countdown pill per satellite row — "AOS 3m 42s" or "LIVE" when overhead. Move speed to expanded detail grid alongside angular rate to reduce compact row clutter. Prefix altitude pill with "Alt" for clarity. Extract fmtCountdown, fmtCountdownCompact, fmtDuration, and fmtDurationClock into shared format.ts, replacing 4 duplicate definitions across BottomPanel, RotatorWindow, PassesWindow, and DopplerWindow. Default selection mode changed to single-select for new users on all platforms.
1 parent cb5a551 commit 9bce2e4

8 files changed

Lines changed: 80 additions & 35 deletions

File tree

src/app.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,7 +1003,7 @@ export class App {
10031003
this.updateObserverMarker();
10041004
if (observerPassDebounce) clearTimeout(observerPassDebounce);
10051005
observerPassDebounce = setTimeout(() => {
1006-
if (uiStore.passesVisible && uiStore.passesTab === 'selected') this.requestPasses();
1006+
if (uiStore.passesTab === 'selected' || this.selectedSats.size > 0) this.requestPasses();
10071007
if (uiStore.passesVisible && uiStore.passesTab === 'nearby') this.requestNearbyPasses();
10081008
}, 500);
10091009
};
@@ -1764,8 +1764,8 @@ export class App {
17641764
// Keep reactive sat count in sync for UI components (view-independent)
17651765
uiStore.selectedSatCount = this.selectedSats.size;
17661766

1767-
// Pass predictor: auto-trigger when selection changes and window is open
1768-
if (uiStore.passesVisible && this.lastPassSatsVersion !== this.selectedSatsVersion) {
1767+
// Pass predictor: auto-trigger when selection changes (for AOS countdown in selection window)
1768+
if (this.lastPassSatsVersion !== this.selectedSatsVersion) {
17691769
// Always allow clearing (0 sats); only gate recomputation on not-busy
17701770
if (this.selectedSats.size === 0 || !this.passPredictor.isComputing()) {
17711771
this.requestPasses();

src/format.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,36 @@ export function formatMHzRange(minMHz: number, maxMHz: number): string {
2929
if (bothMHz) return trimNum(minMHz, 1) + '–' + trimNum(maxMHz, 1) + ' MHz';
3030
return formatMHz(minMHz) + '–' + formatMHz(maxMHz);
3131
}
32+
33+
/** Format seconds as clock-style countdown: "1:05:30" or "5:30". */
34+
export function fmtCountdown(sec: number): string {
35+
if (sec <= 0) return '0:00';
36+
const h = Math.floor(sec / 3600);
37+
const m = Math.floor((sec % 3600) / 60);
38+
const s = Math.round(sec % 60);
39+
return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` : `${m}:${String(s).padStart(2, '0')}`;
40+
}
41+
42+
/** Format seconds as compact countdown: "42s", "3m 42s", "2h 15m", "1d 3h". */
43+
export function fmtCountdownCompact(sec: number): string {
44+
if (sec < 60) return `${Math.floor(sec)}s`;
45+
if (sec < 3600) return `${Math.floor(sec / 60)}m ${Math.floor(sec % 60)}s`;
46+
if (sec < 86400) return `${Math.floor(sec / 3600)}h ${Math.floor((sec % 3600) / 60)}m`;
47+
const d = Math.floor(sec / 86400);
48+
const h = Math.floor((sec % 86400) / 3600);
49+
return `${d}d ${h}h`;
50+
}
51+
52+
/** Format seconds as pass duration: "5m30s". */
53+
export function fmtDuration(sec: number): string {
54+
const m = Math.floor(sec / 60);
55+
const s = Math.round(sec % 60);
56+
return `${m}m${String(s).padStart(2, '0')}s`;
57+
}
58+
59+
/** Format seconds as "m:ss" duration (for charts/timelines). */
60+
export function fmtDurationClock(sec: number): string {
61+
const m = Math.floor(sec / 60);
62+
const s = Math.round(sec % 60);
63+
return `${m}:${String(s).padStart(2, '0')}`;
64+
}

src/stores/ui.svelte.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ class UIStore {
291291
this.showGrid = load('satvisor_grid', false);
292292
this.showSkyGrid = load('satvisor_skygrid', true);
293293
this.radarVfx = load('satvisor_radar_vfx', true);
294-
this.singleSelectMode = load('satvisor_single_select', this.isMobile);
294+
this.singleSelectMode = load('satvisor_single_select', true);
295295
const savedTab = localStorage.getItem('satvisor_passes_tab');
296296
if (savedTab === 'selected' || savedTab === 'nearby') this.passesTab = savedTab;
297297
const savedTimeTab = localStorage.getItem('satvisor_time_tab');

src/ui/BottomPanel.svelte

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,7 @@
33
import { rotatorStore } from '../stores/rotator.svelte';
44
import { beamStore } from '../stores/beam.svelte';
55
import { timeStore } from '../stores/time.svelte';
6-
7-
function fmtCountdown(sec: number): string {
8-
if (sec <= 0) return '0:00';
9-
const h = Math.floor(sec / 3600);
10-
const m = Math.floor((sec % 3600) / 60);
11-
const s = Math.round(sec % 60);
12-
return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` : `${m}:${String(s).padStart(2, '0')}`;
13-
}
6+
import { fmtCountdown } from '../format';
147
158
const hash = __COMMIT_HASH__;
169
const version = __COMMIT_DATE__ ? 'v' + __COMMIT_DATE__.slice(2, 10).replace(/-/g, '') : '';

src/ui/DopplerWindow.svelte

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { observerStore } from '../stores/observer.svelte';
99
import { timeStore } from '../stores/time.svelte';
1010
import { ICON_DOPPLER } from './shared/icons';
11+
import { fmtDurationClock } from '../format';
1112
import { calculateDopplerShift, createSatrec } from '../astro/doppler';
1213
import { epochToDatetimeStr, epochToDate } from '../astro/epoch';
1314
import { satColorCss } from '../constants';
@@ -350,11 +351,7 @@
350351
return hz.toFixed(0) + ' Hz';
351352
}
352353
353-
function formatDuration(sec: number): string {
354-
const m = Math.floor(sec / 60);
355-
const s = Math.round(sec % 60);
356-
return `${m}:${String(s).padStart(2, '0')}`;
357-
}
354+
const formatDuration = fmtDurationClock;
358355
359356
/** Linearly interpolate frequency at tSec from cached data. */
360357
function interpolateFreq(tSec: number): number | null {

src/ui/PassesWindow.svelte

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { observerStore } from '../stores/observer.svelte';
88
import { timeStore } from '../stores/time.svelte';
99
import { ICON_PASSES, ICON_DOPPLER, ICON_ECLIPSE, ICON_SUN, ICON_FILTER } from './shared/icons';
10-
import { formatMHz, formatMHzRange } from '../format';
10+
import { formatMHz, formatMHzRange, fmtDuration } from '../format';
1111
import { satColorCss } from '../constants';
1212
import { epochToDate } from '../astro/epoch';
1313
import { sunLabel } from '../astro/eclipse';
@@ -47,12 +47,6 @@
4747
return `${pad(h)}:${pad(m)}:${pad(s)}`;
4848
}
4949
50-
function formatDuration(sec: number): string {
51-
const m = Math.floor(sec / 60);
52-
const s = Math.round(sec % 60);
53-
return `${m}m${String(s).padStart(2, '0')}s`;
54-
}
55-
5650
function elClass(maxEl: number): string {
5751
if (maxEl < 10) return 'el-low';
5852
if (maxEl < 30) return 'el-mid';
@@ -284,7 +278,7 @@
284278
<span class="sat-name" title={pass.satName}>{pass.satName}</span>
285279
</span>
286280
<span class="td td-time">{formatTime(pass.aosEpoch)} <span class="arrow">&rarr;</span> {formatTime(pass.losEpoch)}</span>
287-
<span class="td td-dur">{formatDuration(pass.durationSec)}</span>
281+
<span class="td td-dur">{fmtDuration(pass.durationSec)}</span>
288282
<span class="td td-el {elClass(pass.maxEl)}">{pass.maxEl.toFixed(1)}&deg;</span>
289283
<span class="td td-mag {magClass(pass)}" title={magTooltip(pass)}>{#if pass.sunAlt > 0 && !pass.eclipsed}<span class="sun-icon">{@html ICON_SUN}</span>{/if}{#if pass.eclipsed}<span class="eclipse-icon">{@html ICON_ECLIPSE}</span>{:else if pass.peakMag !== null}{pass.peakMag.toFixed(1)}{:else}?{/if}</span>
290284
<span class="td td-actions">

src/ui/RotatorWindow.svelte

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,7 @@
1212
import { satColorRgba } from '../constants';
1313
import { ViewMode } from '../types';
1414
import { chart, pointerHitRadius } from './shared/touch-metrics';
15-
16-
function fmtCountdown(sec: number): string {
17-
if (sec <= 0) return '0:00';
18-
const h = Math.floor(sec / 3600);
19-
const m = Math.floor((sec % 3600) / 60);
20-
const s = Math.round(sec % 60);
21-
return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` : `${m}:${String(s).padStart(2, '0')}`;
22-
}
15+
import { fmtCountdown } from '../format';
2316
2417
let parkPos = $derived.by(() => {
2518
if (rotatorStore.parkPreset === 'custom') return { az: rotatorStore.parkAz, el: rotatorStore.parkEl };

src/ui/SelectionWindow.svelte

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,31 @@
1111
import { beamStore } from '../stores/beam.svelte';
1212
import { satColorCss } from '../constants';
1313
import type { SelectedSatInfo } from '../types';
14+
import type { SatellitePass } from '../passes/pass-types';
15+
import { fmtCountdownCompact } from '../format';
16+
17+
/** Find the next upcoming (or active) pass for a satellite */
18+
function nextPass(noradId: number, passes: SatellitePass[], epoch: number): SatellitePass | null {
19+
// Active pass first
20+
for (const p of passes) {
21+
if (p.satNoradId === noradId && epoch >= p.aosEpoch && epoch <= p.losEpoch) return p;
22+
}
23+
// Next upcoming
24+
for (const p of passes) {
25+
if (p.satNoradId === noradId && p.aosEpoch > epoch) return p;
26+
}
27+
return null;
28+
}
29+
30+
function aosLabel(noradId: number): string | null {
31+
const epoch = uiStore.passListEpoch;
32+
if (!epoch) return null;
33+
const pass = nextPass(noradId, uiStore.passes, epoch);
34+
if (!pass) return null;
35+
if (epoch >= pass.aosEpoch && epoch <= pass.losEpoch) return 'LIVE';
36+
const sec = (pass.aosEpoch - epoch) * 86400;
37+
return fmtCountdownCompact(sec);
38+
}
1439
1540
let expandedSats = $state(new Set<number>());
1641
let searchQuery = $state('');
@@ -191,8 +216,11 @@
191216
onchange={() => uiStore.toggleSatVisibility(sat.noradId)} />
192217
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
193218
<span class="sat-name" onclick={() => toggle(sat.noradId)}>{sat.name}</span>
194-
<span class="pill">{fmt(sat.altKm, 0)} km</span>
195-
<span class="pill">{fmt(sat.speedKmS, 2)} km/s</span>
219+
<span class="pill">Alt {fmt(sat.altKm, 0)} km</span>
220+
{#if aosLabel(sat.noradId) != null}
221+
{@const label = aosLabel(sat.noradId)!}
222+
<span class="pill aos-pill" class:aos-live={label === 'LIVE'}>{label === 'LIVE' ? 'LIVE' : `AOS ${label}`}</span>
223+
{/if}
196224
<button class="expand-btn" onclick={() => toggle(sat.noradId)} title={expandedSats.has(sat.noradId) ? 'Collapse' : 'Expand'}>
197225
<svg viewBox="0 0 10 6" width="8" height="5" fill="currentColor" class:rotated={expandedSats.has(sat.noradId)}>
198226
<polygon points="0,0 10,0 5,6"/>
@@ -206,6 +234,7 @@
206234
<span class="dl">NORAD ID</span><span class="dv">{sat.noradId}</span>
207235
<span class="dl">Latitude</span><span class="dv">{fmt(sat.latDeg, 2)}&deg;</span>
208236
<span class="dl">Longitude</span><span class="dv">{fmt(sat.lonDeg, 2)}&deg;</span>
237+
<span class="dl">Speed</span><span class="dv">{fmt(sat.speedKmS, 2)} km/s</span>
209238
<span class="dl">Ang. rate</span><span class="dv">{(sat.angularRateDegS ?? 0).toFixed(2)}°/s</span>
210239
{#if sat.magStr !== null}
211240
<span class="dl">Magnitude</span><span class="dv">{sat.magStr}</span>
@@ -362,6 +391,12 @@
362391
white-space: nowrap;
363392
flex-shrink: 0;
364393
}
394+
.aos-pill {
395+
color: var(--text-muted);
396+
}
397+
.aos-live {
398+
color: var(--live);
399+
}
365400
.expand-btn {
366401
background: none;
367402
border: none;

0 commit comments

Comments
 (0)