chore: release v0.22.9
This commit is contained in:
+82
-4
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Activity, AlertCircle, Antenna, Bell, Check, CheckCircle2, ChevronDown, Clock, CloudOff, Compass, Database, Ear, Eraser, Gauge, Hash, Loader2, Lock,
|
||||
Activity, AlertCircle, Antenna, Bell, Check, CheckCircle2, ChevronDown, Clock, CloudOff, Compass, Database, Ear, Eraser, Flame, Gauge, Hash, Loader2, Lock,
|
||||
Maximize2, Minimize2, Mic, MessageSquare, Pencil, Radio, RadioTower, RefreshCw, Satellite, Send, Settings, SlidersHorizontal, SpellCheck, Square, Terminal, Trash2, Unlock, X, Zap,
|
||||
} from 'lucide-react';
|
||||
|
||||
@@ -77,6 +77,7 @@ import { IcomPanel } from '@/components/IcomPanel';
|
||||
import { YaesuPanel } from '@/components/YaesuPanel';
|
||||
import { AntGeniusPanel, type AGStatus } from '@/components/AntGeniusPanel';
|
||||
import { TunerGeniusPanel, type TGStatus } from '@/components/TunerGeniusPanel';
|
||||
import { AmpWidget } from '@/components/AmpWidget';
|
||||
import { ScpPanel, type ScpResult } from '@/components/ScpPanel';
|
||||
import { FilterBuilder, type QueryFilter } from '@/components/FilterBuilder';
|
||||
import { AwardsPanel } from '@/components/AwardsPanel';
|
||||
@@ -570,6 +571,16 @@ export default function App() {
|
||||
// PGXL's OPERATE state comes from the radio when it reports the amp.
|
||||
const [ampSts, setAmpSts] = useState<any[]>([]);
|
||||
const [flexAmp, setFlexAmp] = useState<any>(null);
|
||||
// Amplifier widget: whether it is shown, and which amp — "all", or one amp id.
|
||||
// Several amps side by side make a wide widget, so an operator running two
|
||||
// picks the one he watches while transmitting. Declared here because the poll
|
||||
// below runs faster while the widget is open.
|
||||
const [showAmpWidget, setShowAmpWidget] = useState(() => localStorage.getItem('opslog.showAmpWidget') !== '0');
|
||||
const [ampWidgetSel, setAmpWidgetSel] = useState(() => localStorage.getItem('opslog.ampSel.widget') || 'all');
|
||||
// Poll fast only while the amplifier widget is open: its meters must track TX
|
||||
// the way the FlexRadio panel's do (400 ms), or the power reads seconds behind
|
||||
// the transmission and looks like the app is struggling. The status-bar chips
|
||||
// alone need nothing like that rate.
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
const tick = () => Promise.all([
|
||||
@@ -577,9 +588,9 @@ export default function App() {
|
||||
GetFlexState().catch(() => null),
|
||||
]).then(([l, fx]: any[]) => { if (alive) { setAmpSts((l ?? []) as any[]); setFlexAmp(fx); } });
|
||||
tick();
|
||||
const id = window.setInterval(tick, 2000);
|
||||
const id = window.setInterval(tick, showAmpWidget ? 400 : 2000);
|
||||
return () => { alive = false; window.clearInterval(id); };
|
||||
}, []);
|
||||
}, [showAmpWidget]);
|
||||
// Multi-op "who's on air" widget: every operator's live status from the shared
|
||||
// MySQL logbook (freq/mode/version). Only polled on a MySQL logbook.
|
||||
type LiveStation = { operator: string; station: string; freq_hz: number; band: string; mode: string; online: boolean; version: string; age_sec: number };
|
||||
@@ -4849,6 +4860,60 @@ export default function App() {
|
||||
{showTuner && tgStatus.connected && <span className="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-success" />}
|
||||
</button>
|
||||
)}
|
||||
{/* Amplifier widget. With one amp the icon is a plain toggle; with
|
||||
several it also carries a dropdown to choose which one the widget
|
||||
shows (or All), since two amps stacked make a tall widget. */}
|
||||
{ampSts.length > 0 && (
|
||||
<div className="relative inline-flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { const v = !showAmpWidget; setShowAmpWidget(v); writeUiPref('opslog.showAmpWidget', v ? '1' : '0'); }}
|
||||
title={showAmpWidget ? t('ampw.hideHint') : t('ampw.showHint')}
|
||||
className={cn(
|
||||
'relative inline-flex items-center justify-center size-7 rounded-md border transition-colors',
|
||||
ampSts.length > 1 && 'rounded-r-none border-r-0',
|
||||
showAmpWidget ? 'border-success-border bg-success-muted text-success-muted-foreground hover:bg-success-muted'
|
||||
: 'border-border text-muted-foreground hover:bg-muted',
|
||||
)}
|
||||
>
|
||||
<Flame className="size-4" />
|
||||
{showAmpWidget && ampSts.some((a: any) => a.spe?.connected || a.acom?.connected || a.pgxl?.connected || flexAmp?.amp_available) && (
|
||||
<span className="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-success" />
|
||||
)}
|
||||
</button>
|
||||
{ampSts.length > 1 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button type="button" title={t('ampw.pick')}
|
||||
className={cn('inline-flex items-center justify-center h-7 w-4 rounded-md rounded-l-none border transition-colors',
|
||||
showAmpWidget ? 'border-success-border bg-success-muted text-success-muted-foreground hover:bg-success-muted'
|
||||
: 'border-border text-muted-foreground hover:bg-muted')}>
|
||||
<ChevronDown className="size-3" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="min-w-44">
|
||||
{[{ id: 'all', name: t('ampw.all') }, ...ampSts.map((a: any) => ({ id: a.id, name: a.name || a.id }))].map((o) => (
|
||||
<DropdownMenuItem key={o.id}
|
||||
onSelect={() => {
|
||||
setAmpWidgetSel(o.id);
|
||||
writeUiPref('opslog.ampSel.widget', o.id);
|
||||
// Picking an amp means "show me this one" — opening the
|
||||
// widget too saves a second click on a hidden one.
|
||||
if (!showAmpWidget) { setShowAmpWidget(true); writeUiPref('opslog.showAmpWidget', '1'); }
|
||||
}}>
|
||||
<span className="flex items-center gap-2 min-w-0">
|
||||
{ampWidgetSel === o.id
|
||||
? <Check className="size-3.5 shrink-0 text-primary" />
|
||||
: <span className="size-3.5 shrink-0" />}
|
||||
<span className="truncate">{o.name}</span>
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{scpEnabled && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -5365,7 +5430,7 @@ export default function App() {
|
||||
{/* Reserved free space to the right. The WinKeyer CW keyer and/or the
|
||||
Digital Voice Keyer take this slot when enabled (Log4OM-style);
|
||||
otherwise it shows the QRZ profile photo. */}
|
||||
{!compact && (chatShown || wkEnabled || dvkEnabled || lookupResult?.image_url || (showRotor && (rotatorHeading.enabled || dxPath)) || (showAntGenius && agEnabled) || (showTuner && tgEnabled) || (showScp && scpEnabled) || (showLiveStations && dbConn?.backend === 'mysql')) && (
|
||||
{!compact && (chatShown || wkEnabled || dvkEnabled || lookupResult?.image_url || (showRotor && (rotatorHeading.enabled || dxPath)) || (showAntGenius && agEnabled) || (showTuner && tgEnabled) || (showAmpWidget && ampSts.length > 0) || (showScp && scpEnabled) || (showLiveStations && dbConn?.backend === 'mysql')) && (
|
||||
// relative + absolute inner (like the F1-F5 panel): a taller widget (e.g.
|
||||
// the DVK with Auto CQ) can't grow the row — the row height stays set by
|
||||
// the entry strip and each widget fills that height, scrolling inside.
|
||||
@@ -5452,6 +5517,19 @@ export default function App() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showAmpWidget && ampSts.length > 0 && (
|
||||
// One column per amplifier shown, so two amps stand side by side
|
||||
// rather than making the widget twice as tall as the dock row.
|
||||
<div className="shrink-0 min-h-0"
|
||||
style={{ width: `${Math.min(ampWidgetSel === 'all' ? ampSts.length : 1, 3) * 250 + 20}px` }}>
|
||||
<AmpWidget
|
||||
amps={ampSts}
|
||||
flex={flexAmp}
|
||||
sel={ampWidgetSel}
|
||||
onClose={() => { setShowAmpWidget(false); writeUiPref('opslog.showAmpWidget', '0'); }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showTuner && tgEnabled && (
|
||||
<div className="w-[230px] shrink-0 min-h-0">
|
||||
<TunerGeniusPanel
|
||||
|
||||
Reference in New Issue
Block a user