mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-01 11:02:56 +02:00
Add local tunable for glittering status dot. Closes #200.
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getStateKey } from '../utils/conversationState';
|
||||
import { mergeContactIntoList } from '../utils/contactMerge';
|
||||
import { getContactDisplayName } from '../utils/pubkey';
|
||||
import { appendRawPacketUnique } from '../utils/rawPacketIdentity';
|
||||
import { emitStatusDotPulse } from '../utils/statusDotPulse';
|
||||
import type {
|
||||
Channel,
|
||||
Contact,
|
||||
@@ -253,6 +254,7 @@ export function useRealtimeAppState({
|
||||
},
|
||||
onRawPacket: (packet: RawPacket) => {
|
||||
recordRawPacketObservation?.(packet);
|
||||
emitStatusDotPulse(packet.payload_type);
|
||||
setRawPackets((prev) => appendRawPacketUnique(prev, packet, maxRawPackets));
|
||||
},
|
||||
onMessageAcked: (
|
||||
|
||||
61
frontend/src/utils/statusDotPulse.ts
Normal file
61
frontend/src/utils/statusDotPulse.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
export const STATUS_DOT_PULSE_CHANGE_EVENT = 'remoteterm-status-dot-pulse-change';
|
||||
export const STATUS_DOT_PULSE_PACKET_EVENT = 'remoteterm-status-dot-pulse-packet';
|
||||
|
||||
const STORAGE_KEY = 'remoteterm-status-dot-pulse';
|
||||
|
||||
export type StatusDotPulseKind = 'channel' | 'dm' | 'advert' | 'other';
|
||||
|
||||
export function getStatusDotPulseEnabled(): boolean {
|
||||
try {
|
||||
return localStorage.getItem(STORAGE_KEY) === 'true';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function setStatusDotPulseEnabled(enabled: boolean): void {
|
||||
try {
|
||||
if (enabled) {
|
||||
localStorage.setItem(STORAGE_KEY, 'true');
|
||||
} else {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
} catch {
|
||||
// localStorage may be unavailable
|
||||
}
|
||||
}
|
||||
|
||||
export function payloadTypeToPulseKind(payloadType: string | null | undefined): StatusDotPulseKind {
|
||||
switch (payloadType) {
|
||||
case 'GROUP_TEXT':
|
||||
return 'channel';
|
||||
case 'TEXT_MESSAGE':
|
||||
return 'dm';
|
||||
case 'ADVERT':
|
||||
return 'advert';
|
||||
default:
|
||||
return 'other';
|
||||
}
|
||||
}
|
||||
|
||||
const PULSE_COLORS: Record<StatusDotPulseKind, string> = {
|
||||
channel: 'hsl(210, 90%, 55%)', // blue
|
||||
dm: 'hsl(270, 75%, 60%)', // purple
|
||||
advert: 'hsl(185, 85%, 55%)', // cyan
|
||||
other: 'hsl(140, 80%, 22%)', // dark green
|
||||
};
|
||||
|
||||
export function pulseColorFor(kind: StatusDotPulseKind): string {
|
||||
return PULSE_COLORS[kind];
|
||||
}
|
||||
|
||||
export const STATUS_DOT_PULSE_DURATION_MS = 250;
|
||||
|
||||
export function emitStatusDotPulse(payloadType: string | null | undefined): void {
|
||||
const kind = payloadTypeToPulseKind(payloadType);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent<StatusDotPulseKind>(STATUS_DOT_PULSE_PACKET_EVENT, {
|
||||
detail: kind,
|
||||
})
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user