Add local tunable for glittering status dot. Closes #200.

This commit is contained in:
Jack Kingsman
2026-04-16 12:40:30 -07:00
parent e8c50d0b2a
commit 5cd8f7e80f
4 changed files with 133 additions and 1 deletions
+46 -1
View File
@@ -19,6 +19,14 @@ import {
getShowBatteryVoltage,
mvToPercent,
} from '../utils/batteryDisplay';
import {
STATUS_DOT_PULSE_CHANGE_EVENT,
STATUS_DOT_PULSE_DURATION_MS,
STATUS_DOT_PULSE_PACKET_EVENT,
getStatusDotPulseEnabled,
pulseColorFor,
type StatusDotPulseKind,
} from '../utils/statusDotPulse';
import { cn } from '@/lib/utils';
interface StatusBarProps {
@@ -85,6 +93,40 @@ export function StatusBar({
: 'Radio Disconnected';
const [reconnecting, setReconnecting] = useState(false);
const [currentTheme, setCurrentTheme] = useState(getSavedTheme);
const [pulseEnabled, setPulseEnabled] = useState(getStatusDotPulseEnabled);
const [pulseKind, setPulseKind] = useState<StatusDotPulseKind | null>(null);
useEffect(() => {
const handler = () => setPulseEnabled(getStatusDotPulseEnabled());
window.addEventListener(STATUS_DOT_PULSE_CHANGE_EVENT, handler);
return () => window.removeEventListener(STATUS_DOT_PULSE_CHANGE_EVENT, handler);
}, []);
useEffect(() => {
if (!pulseEnabled) {
setPulseKind(null);
return;
}
let timer: number | null = null;
const handler = (event: Event) => {
const kind = (event as CustomEvent<StatusDotPulseKind>).detail;
setPulseKind(kind);
if (timer !== null) {
window.clearTimeout(timer);
}
timer = window.setTimeout(() => {
setPulseKind(null);
timer = null;
}, STATUS_DOT_PULSE_DURATION_MS);
};
window.addEventListener(STATUS_DOT_PULSE_PACKET_EVENT, handler);
return () => {
window.removeEventListener(STATUS_DOT_PULSE_PACKET_EVENT, handler);
if (timer !== null) {
window.clearTimeout(timer);
}
};
}, [pulseEnabled]);
useEffect(() => {
const handleThemeChange = (event: Event) => {
@@ -154,9 +196,12 @@ export function StatusBar({
radioState === 'initializing' || radioState === 'connecting'
? 'bg-warning'
: connected
? 'bg-status-connected shadow-[0_0_6px_hsl(var(--status-connected)/0.5)]'
? pulseKind
? ''
: 'bg-status-connected shadow-[0_0_6px_hsl(var(--status-connected)/0.5)]'
: 'bg-status-disconnected'
)}
style={connected && pulseKind ? { backgroundColor: pulseColorFor(pulseKind) } : undefined}
aria-hidden="true"
/>
<span className="hidden lg:inline text-muted-foreground">{statusLabel}</span>
@@ -35,6 +35,11 @@ import {
getShowBatteryVoltage,
setShowBatteryVoltage as saveBatteryVoltage,
} from '../../utils/batteryDisplay';
import {
STATUS_DOT_PULSE_CHANGE_EVENT,
getStatusDotPulseEnabled,
setStatusDotPulseEnabled as saveStatusDotPulse,
} from '../../utils/statusDotPulse';
export function SettingsLocalSection({
onLocalLabelChange,
@@ -52,6 +57,7 @@ export function SettingsLocalSection({
const [autoFocusInput, setAutoFocusInput] = useState(getAutoFocusInputEnabled);
const [batteryPercent, setBatteryPercent] = useState(getShowBatteryPercent);
const [batteryVoltage, setBatteryVoltage] = useState(getShowBatteryVoltage);
const [statusDotPulse, setStatusDotPulse] = useState(getStatusDotPulseEnabled);
const [fontScale, setFontScale] = useState(getSavedFontScale);
const [fontScaleSlider, setFontScaleSlider] = useState(getSavedFontScale);
const [fontScaleInput, setFontScaleInput] = useState(() => String(getSavedFontScale()));
@@ -222,6 +228,24 @@ export function SettingsLocalSection({
</p>
)}
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={statusDotPulse}
onChange={(e) => {
const v = e.target.checked;
setStatusDotPulse(v);
saveStatusDotPulse(v);
window.dispatchEvent(new Event(STATUS_DOT_PULSE_CHANGE_EVENT));
}}
className="w-4 h-4 rounded border-input accent-primary"
/>
<span className="text-sm">
Glitter status dot as packets arrive (blue = channel, purple = DM, cyan = advert, dark
green = other)
</span>
</label>
<div className="space-y-3">
<Label htmlFor="font-scale-input">Relative Font Size</Label>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">