chore: release v0.20.7
This commit is contained in:
@@ -42,7 +42,7 @@ import {
|
||||
QSOAudioBegin, QSOAudioCancel, QSOAudioRestart, QSOAudioResetClock,
|
||||
GetAwardDefs,
|
||||
GetUIPref, GetActiveProfile, QuitApp,
|
||||
ReportLiveActivity, GetLiveStatusEnabled, LiveLastQSOAgeSec,
|
||||
ReportLiveActivity, LiveLastQSOAgeSec,
|
||||
} from '../wailsjs/go/main/App';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { applyAwardRefs } from '@/lib/awardRefs';
|
||||
@@ -1121,22 +1121,22 @@ export default function App() {
|
||||
useEffect(() => { if (!showSettings) setShowBeamOnMap(localStorage.getItem('opslog.showBeamOnMap') !== '0'); }, [showSettings]);
|
||||
// "ON AIR" status-bar badge: mirrors the multi-op live status this operator
|
||||
// publishes — online (blinking) when a QSO was logged in the last 5 min, else
|
||||
// offline. Only shown when live-status publishing is enabled (Settings→General).
|
||||
const [liveStatusOn, setLiveStatusOn] = useState(false);
|
||||
// offline. Publishing is always on for a shared MySQL logbook (no user toggle:
|
||||
// 5 min without a QSO removes the row automatically), so the badge simply
|
||||
// follows the backend.
|
||||
const [onAir, setOnAir] = useState(false);
|
||||
useEffect(() => { if (!showSettings) GetLiveStatusEnabled().then((v) => setLiveStatusOn(!!v)).catch(() => {}); }, [showSettings]);
|
||||
useEffect(() => {
|
||||
// Read the ON-AIR state straight from the backend (single source of truth:
|
||||
// liveLastQSOAt, stamped on every log and seeded from the DB at launch). Poll
|
||||
// it + refresh on each logged QSO — no fragile frontend timestamp to drift.
|
||||
const refresh = () => LiveLastQSOAgeSec()
|
||||
.then((sec: number) => setOnAir(liveStatusOn && typeof sec === 'number' && sec >= 0 && sec < 300))
|
||||
.then((sec: number) => setOnAir(typeof sec === 'number' && sec >= 0 && sec < 300))
|
||||
.catch(() => {});
|
||||
refresh();
|
||||
const off = EventsOn('qso:logged', refresh);
|
||||
const id = window.setInterval(refresh, 5 * 1000); // responsive without hammering (cheap 400-row scan)
|
||||
const id = window.setInterval(refresh, 5 * 1000); // responsive without hammering (single indexed row)
|
||||
return () => { off(); window.clearInterval(id); };
|
||||
}, [liveStatusOn]);
|
||||
}, []);
|
||||
// QSO-rate meter (10/60 min) in the header — opt-in via Settings→General.
|
||||
const [showQsoRate, setShowQsoRate] = useState(() => localStorage.getItem('opslog.showQsoRate') === '1');
|
||||
useEffect(() => { if (!showSettings) setShowQsoRate(localStorage.getItem('opslog.showQsoRate') === '1'); }, [showSettings]);
|
||||
@@ -5122,7 +5122,7 @@ export default function App() {
|
||||
disabled={!rotatorHeading.enabled}
|
||||
onClick={() => { setSettingsSection('rotator'); setShowSettings(true); }}
|
||||
/>
|
||||
{liveStatusOn && (
|
||||
{liveStationsOn && (
|
||||
<div
|
||||
className={cn('inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-[11px] font-bold uppercase tracking-wider shrink-0 transition-colors',
|
||||
onAir ? 'border-danger-border bg-danger-muted text-danger-muted-foreground' : 'border-border text-muted-foreground')}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
FlexSetProcessor, FlexSetProcessorLevel, FlexSetMon, FlexSetMonLevel, FlexSetMic,
|
||||
FlexMox, FlexAmpOperate,
|
||||
GetPGXLStatus, PGXLSetFanMode, GetPGXLSettings, GetSPEStatus, SPESetOperate, SPESetPower, SPESetPowerLevel,
|
||||
GetACOMStatus, ACOMSetOperate, ACOMSetPower,
|
||||
FlexSetAGCMode, FlexSetAGCThreshold, FlexSetAudioLevel, FlexSetMute, FlexSetRXAntenna, FlexSetTXAntenna, FlexSetSplit, FlexSetActiveSlice, FlexSetTXSlice,
|
||||
FlexSetRIT, FlexSetRITFreq, FlexSetXIT, FlexSetXITFreq,
|
||||
FlexSetNB, FlexSetNBLevel, FlexSetNR, FlexSetNRLevel, FlexSetANF, FlexSetANFLevel,
|
||||
@@ -352,7 +353,8 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
||||
const id = window.setInterval(load, 5000);
|
||||
return () => { alive = false; window.clearInterval(id); };
|
||||
}, []);
|
||||
const isSPE = ampEnabled && ampType !== 'pgxl';
|
||||
const isACOM = ampEnabled && ampType.startsWith('acom');
|
||||
const isSPE = ampEnabled && !isACOM && ampType !== 'pgxl';
|
||||
// SPE Expert live status (only polled when an SPE amp is configured).
|
||||
const [spe, setSpe] = useState<any>({ connected: false });
|
||||
useEffect(() => {
|
||||
@@ -363,6 +365,16 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
||||
const id = window.setInterval(tick, 1500);
|
||||
return () => { alive = false; window.clearInterval(id); };
|
||||
}, [isSPE]);
|
||||
// ACOM live status (only polled when an ACOM amp is configured).
|
||||
const [acom, setAcom] = useState<any>({ connected: false });
|
||||
useEffect(() => {
|
||||
if (!isACOM) return;
|
||||
let alive = true;
|
||||
const tick = () => GetACOMStatus().then((s: any) => alive && setAcom(s || { connected: false })).catch(() => {});
|
||||
tick();
|
||||
const id = window.setInterval(tick, 1500);
|
||||
return () => { alive = false; window.clearInterval(id); };
|
||||
}, [isACOM]);
|
||||
|
||||
const change = (key: keyof FlexState, val: number | boolean | string, send: () => Promise<any>) => {
|
||||
hold.current[key] = Date.now() + 900;
|
||||
@@ -872,10 +884,55 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* ACOM amplifier (serial/TCP) — shown when it's the configured amp. Driven
|
||||
by OpsLog's own ACOM link (the Flex doesn't report ACOM amps). */}
|
||||
{isACOM && (
|
||||
<Card icon={Flame} title={`${t('flxp.amplifier')} · ACOM${acom.model ? ' ' + acom.model : ''}`} accent="#ea580c">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<button type="button" disabled={!acom.connected}
|
||||
onClick={() => ACOMSetOperate(!acom.operate).catch(() => {})}
|
||||
className={cn('px-4 py-2 rounded-lg text-sm font-extrabold tracking-wide border-2 transition-all disabled:opacity-30',
|
||||
acom.operate ? 'bg-warning text-warning-foreground border-warning shadow-[0_0_14px] shadow-warning/50' : 'bg-card text-warning border-warning hover:bg-warning-muted')}>
|
||||
{acom.operate ? 'OPERATE' : 'STANDBY'}
|
||||
</button>
|
||||
{/* Power ON is a DTR/RTS pulse — serial only, and it works while the amp
|
||||
is off (the port stays open), so gate on port_open not connected. */}
|
||||
<div className="inline-flex rounded-lg overflow-hidden border-2 border-success/70">
|
||||
<button type="button" disabled={!(acom.port_open && acom.transport === 'serial')}
|
||||
onClick={() => ACOMSetPower(true).catch(() => {})}
|
||||
title={acom.transport === 'serial' ? 'Power on (DTR/RTS pulse — needs the power-on pins wired)' : 'Power-on needs the serial DTR/RTS lines — not available over a network bridge'}
|
||||
className="px-3 py-2 text-sm font-bold bg-card text-success hover:bg-success/15 disabled:opacity-30">ON</button>
|
||||
<button type="button" disabled={!acom.connected}
|
||||
onClick={() => ACOMSetPower(false).catch(() => {})}
|
||||
className="px-3 py-2 text-sm font-bold bg-card text-danger border-l-2 border-success/70 hover:bg-danger/15 disabled:opacity-30">OFF</button>
|
||||
</div>
|
||||
<span className={cn('inline-flex items-center gap-1.5 text-sm', acom.connected ? 'text-muted-foreground' : 'text-danger')}>
|
||||
<span className={cn('size-2 rounded-full', acom.connected ? 'bg-success' : 'bg-danger')} />
|
||||
{acom.connected ? acom.state : t('flxp.acomOffline')}
|
||||
</span>
|
||||
{acom.connected && (
|
||||
<span className="text-sm font-mono text-muted-foreground tabular-nums">
|
||||
{acom.band ? `${acom.band} · ` : ''}{acom.fwd_w}W · SWR {Number(acom.swr ?? 0).toFixed(1)} · {acom.temp_c}°C · Fan {acom.fan}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex-1" />
|
||||
{acom.err_text && (
|
||||
<span className="px-2 py-1 rounded bg-danger-muted text-danger-muted-foreground text-xs font-bold">⚠ {acom.err_text}</span>
|
||||
)}
|
||||
</div>
|
||||
{acom.connected && (
|
||||
<MeterBar label={t('flxp.outputPower')} value={Number(acom.fwd_w) || 0} unit="W"
|
||||
lo={0} hi={Number(acom.max_w) || 800}
|
||||
display={`${Number(acom.fwd_w) || 0} W`}
|
||||
segColor={(f) => (f > 0.9 ? '#dc2626' : f > 0.75 ? '#f59e0b' : '#ea580c')} />
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* External amplifier (PowerGenius XL) — only when the Flex reports one AND
|
||||
PowerGenius is the selected amp type. Running an SPE Expert hides this
|
||||
Flex-reported card so the two amps don't both show. */}
|
||||
{st.amp_available && !isSPE && (
|
||||
PowerGenius is the selected amp type. Running an SPE Expert or ACOM hides
|
||||
this Flex-reported card so two amps don't both show. */}
|
||||
{st.amp_available && !isSPE && !isACOM && (
|
||||
<Card icon={Flame} title={`${t('flxp.amplifier')}${st.amp_model ? ' · ' + st.amp_model : ''}`} accent="#ea580c">
|
||||
<div className="flex items-center gap-3">
|
||||
<button type="button" disabled={off}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
GetRotatorSettings, SaveRotatorSettings, TestRotator, RotatorPark, RotatorStop,
|
||||
GetUltrabeamSettings, SaveUltrabeamSettings, TestUltrabeam,
|
||||
GetAntGeniusSettings, SaveAntGeniusSettings,
|
||||
GetPGXLSettings, SavePGXLSettings, GetSPEStatus, SPESetOperate,
|
||||
GetPGXLSettings, SavePGXLSettings, GetSPEStatus, SPESetOperate, GetACOMStatus, ACOMSetOperate,
|
||||
GetWinkeyerSettings, SaveWinkeyerSettings, ListSerialPorts,
|
||||
GetAudioSettings, SaveAudioSettings, ListAudioInputDevices, ListAudioOutputDevices, PickAudioFolder, TestPTT,
|
||||
GetClublogCtyInfo, SetClublogCtyEnabled, DownloadClublogCty,
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
GetMySQLSettings, SaveMySQLSettings, TestMySQLConnection, GetDBBackendStatus,
|
||||
GetAutostartPrograms, SaveAutostartPrograms, BrowseExecutable, LaunchAutostartProgram,
|
||||
GetTelemetryEnabled, SetTelemetryEnabled,
|
||||
GetLiveStatusEnabled, SetLiveStatusEnabled,
|
||||
GetQSLDefaults, SaveQSLDefaults,
|
||||
GetExternalServices, SaveExternalServices, TestQRZUpload, TestClublogUpload, TestHRDLogUpload, TestEQSLUpload,
|
||||
GetPOTAToken, SavePOTAToken,
|
||||
@@ -566,26 +565,6 @@ function TelemetryToggle() {
|
||||
);
|
||||
}
|
||||
|
||||
// LiveStatusToggle publishes this operator's current activity (call + band +
|
||||
// freq + mode) to the shared MySQL `live_status` table every ~15s, for multi-op
|
||||
// events — a small web script on your server renders it for the QRZ page. Only
|
||||
// useful on a MySQL logbook. Self-contained component (owns its async state).
|
||||
function LiveStatusToggle() {
|
||||
const { t } = useI18n();
|
||||
const [on, setOn] = useState(false);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
useEffect(() => {
|
||||
GetLiveStatusEnabled().then((v) => setOn(!!v)).catch(() => {}).finally(() => setLoaded(true));
|
||||
}, []);
|
||||
return (
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={on} disabled={!loaded}
|
||||
onCheckedChange={(c) => { const v = !!c; setOn(v); SetLiveStatusEnabled(v).catch(() => {}); }} />
|
||||
{t('settings.liveStatus')} <span className="text-xs text-muted-foreground">({t('settings.liveStatusHint')})</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
// ADIFMonitorPanel watches a list of external ADIF files (fldigi RTTY, N1MM,
|
||||
// VarAC…) and auto-imports newly appended QSOs — deliberately option-free: a
|
||||
// contact that arrives is imported and uploaded automatically like any log entry.
|
||||
@@ -691,6 +670,47 @@ function SPEStatusCard() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Live ACOM amplifier status + OPERATE/STANDBY. Module-scoped for the same
|
||||
// remount reason as SPEStatusCard. Polls once a second while shown.
|
||||
function ACOMStatusCard() {
|
||||
const [st, setSt] = useState<any>({ connected: false });
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
const tick = () => GetACOMStatus().then((s) => alive && setSt(s || {})).catch(() => {});
|
||||
tick();
|
||||
const id = window.setInterval(tick, 1000);
|
||||
return () => { alive = false; window.clearInterval(id); };
|
||||
}, []);
|
||||
const operate = !!st.operate;
|
||||
return (
|
||||
<div className="rounded-md border border-border p-3 space-y-2 text-xs max-w-xl">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn('size-2 rounded-full', st.connected ? 'bg-success animate-pulse' : 'bg-danger')} />
|
||||
<span className="font-semibold">{st.connected ? `ACOM ${st.model || ''}` : `ACOM ${st.model || ''} — not connected`}</span>
|
||||
{!st.connected && st.last_error && <span className="text-danger truncate text-[10px]">{st.last_error}</span>}
|
||||
<span className="flex-1" />
|
||||
<Button size="sm" variant={operate ? 'default' : 'outline'} disabled={!st.connected}
|
||||
onClick={() => ACOMSetOperate(!operate).catch(() => {})}
|
||||
title="Toggle OPERATE / STANDBY">
|
||||
{operate ? 'OPERATE' : 'STANDBY'}
|
||||
</Button>
|
||||
</div>
|
||||
{st.connected && (
|
||||
<div className="grid grid-cols-4 gap-x-3 gap-y-1 font-mono text-[11px]">
|
||||
<div>{st.state}</div>
|
||||
<div>Band {st.band || '—'}</div>
|
||||
<div>{st.fwd_w} W</div>
|
||||
<div>SWR {Number(st.swr ?? 0).toFixed(2)}</div>
|
||||
<div>Refl {st.refl_w} W</div>
|
||||
<div>Drive {st.drive_w} W</div>
|
||||
<div>{st.temp_c}°C</div>
|
||||
<div>Fan {st.fan}</div>
|
||||
{st.err_text && <div className="col-span-4 text-warning">⚠ {st.err_text} ({st.err_code})</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function RelayAutoPanel() {
|
||||
const { t } = useI18n();
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
@@ -2690,7 +2710,32 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
|
||||
function PGXLPanelSettings() {
|
||||
const isPGXL = pgxl.type === 'pgxl';
|
||||
const isACOM = (pgxl.type || '').startsWith('acom');
|
||||
const isSerial = pgxl.transport === 'serial';
|
||||
// The stored `type` stays the single flat value ("spe13", "acom700", "pgxl" —
|
||||
// binding compatibility); the UI presents it as brand + model.
|
||||
const brand = isPGXL ? 'pgxl' : isACOM ? 'acom' : 'spe';
|
||||
const brandModels: Record<string, { value: string; label: string }[]> = {
|
||||
spe: [
|
||||
{ value: 'spe13', label: 'Expert 1.3K-FA' },
|
||||
{ value: 'spe15', label: 'Expert 1.5K-FA' },
|
||||
{ value: 'spe2k', label: 'Expert 2K-FA' },
|
||||
],
|
||||
acom: [
|
||||
{ value: 'acom500', label: '500S' },
|
||||
{ value: 'acom600', label: '600S' },
|
||||
{ value: 'acom700', label: '700S' },
|
||||
{ value: 'acom1200', label: '1200S' },
|
||||
{ value: 'acom2020', label: '2020S' },
|
||||
],
|
||||
};
|
||||
// Each family has a fixed serial speed: SPE talks 115200, the ACOM S-series is
|
||||
// 9600 8N1 — preset it so switching brand just works. PGXL is TCP-only.
|
||||
const applyType = (v: string) => setPgxl((s) => ({
|
||||
...s, type: v,
|
||||
transport: v === 'pgxl' ? 'tcp' : s.transport,
|
||||
baud: v.startsWith('acom') ? 9600 : v.startsWith('spe') ? 115200 : s.baud,
|
||||
}));
|
||||
return (
|
||||
<>
|
||||
<SectionHeader title="Amplifier" />
|
||||
@@ -2700,21 +2745,44 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
Enable amplifier control
|
||||
</label>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* Connection gets the widest column — "Network (RS232-to-Ethernet)" was
|
||||
truncated with 3 equal columns. */}
|
||||
<div className="grid grid-cols-[1fr_1fr_1.7fr] gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label>Amplifier</Label>
|
||||
<Select value={pgxl.type} onValueChange={(v) => setPgxl((s) => ({ ...s, type: v, transport: v === 'pgxl' ? 'tcp' : s.transport }))}>
|
||||
<Label>Brand</Label>
|
||||
<Select value={brand} onValueChange={(b) => {
|
||||
if (b === brand) return;
|
||||
applyType(b === 'pgxl' ? 'pgxl' : brandModels[b][0].value);
|
||||
}}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="pgxl">4O3A PowerGenius XL</SelectItem>
|
||||
<SelectItem value="spe13">SPE Expert 1.3K-FA</SelectItem>
|
||||
<SelectItem value="spe15">SPE Expert 1.5K-FA</SelectItem>
|
||||
<SelectItem value="spe2k">SPE Expert 2K-FA</SelectItem>
|
||||
<SelectItem value="pgxl">4O3A</SelectItem>
|
||||
<SelectItem value="spe">SPE</SelectItem>
|
||||
<SelectItem value="acom">ACOM</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* PowerGenius is TCP-only; SPE amps connect over USB serial or an
|
||||
RS232-to-Ethernet bridge, so they offer both. */}
|
||||
<div className="space-y-1">
|
||||
<Label>Model</Label>
|
||||
{isPGXL ? (
|
||||
// The PowerGenius is the brand's single model — fixed, no choice.
|
||||
<Select value="pgxl" disabled>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="pgxl">PowerGenius XL</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Select value={pgxl.type} onValueChange={applyType}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{brandModels[brand].map((m) => <SelectItem key={m.value} value={m.value}>{m.label}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
{/* PowerGenius is TCP-only; SPE and ACOM amps connect over USB serial or
|
||||
an RS232-to-Ethernet bridge, so they offer both. */}
|
||||
{!isPGXL && (
|
||||
<div className="space-y-1">
|
||||
<Label>Connection</Label>
|
||||
@@ -2767,12 +2835,17 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isPGXL && pgxl.enabled && <SPEStatusCard />}
|
||||
{!isPGXL && (
|
||||
{!isPGXL && pgxl.enabled && (isACOM ? <ACOMStatusCard /> : <SPEStatusCard />)}
|
||||
{!isPGXL && !isACOM && (
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
SPE control uses the amplifier's proprietary serial protocol (OPERATE toggle + live status). Save to (re)connect. Band / power-level / antenna are still managed on the amp from the transceiver CAT.
|
||||
</p>
|
||||
)}
|
||||
{isACOM && (
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
ACOM control uses the amplifier's serial protocol (9600 8N1): OPERATE / STANDBY / OFF + live telemetry. Save to (re)connect. Power-ON needs the serial DTR/RTS pins wired in the cable (not available over a network bridge). Band tracking stays on the amp itself.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -4515,7 +4588,6 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
{t('gen.checkUpdates')} <span className="text-xs text-muted-foreground">{t('gen.checkUpdatesHint')}</span>
|
||||
</label>
|
||||
<TelemetryToggle />
|
||||
<LiveStatusToggle />
|
||||
|
||||
<MainViewPanes onChanged={onMainPaneChanged} flexAvailable={flexAvailable} icomAvailable={icomAvailable} />
|
||||
|
||||
|
||||
@@ -92,8 +92,6 @@ const en: Dict = {
|
||||
'settings.theme': 'Theme', 'settings.themeHint': 'Interface colour theme.',
|
||||
'settings.telemetry': 'Send anonymous usage statistics',
|
||||
'settings.telemetryHint': 'install ID + version + OS, once a day — no callsign or QSO data',
|
||||
'settings.liveStatus': 'Publish live operator status',
|
||||
'settings.liveStatusHint': 'multi-op on shared MySQL — feeds a QRZ live page',
|
||||
'settings.mainView': 'Main view',
|
||||
'settings.mainViewHint': 'Choose what the Main tab shows on each side (per profile).',
|
||||
'settings.leftPane': 'Left pane', 'settings.rightPane': 'Right pane',
|
||||
@@ -283,7 +281,7 @@ const en: Dict = {
|
||||
'agp.portDeselect': 'Port {letter} — click to deselect', 'agp.portSelect': 'Select on port {letter}', 'agp.online': 'online', 'agp.offline': 'offline', 'agp.close': 'Close', 'agp.connecting': 'Connecting…', 'agp.noAntennas': 'No antennas configured.', 'agp.filterOnHint': 'Showing antennas for {band} only — click to show all bands', 'agp.filterOffHint': 'Showing all antennas — click to show only the current band',
|
||||
'flxp.ritHint': 'RIT — shifts your RECEIVE frequency only. Wheel, ± or arrow keys to adjust (Ctrl = 100 Hz). The offset is kept when you switch it off.', 'flxp.xitHint': 'XIT — shifts your TRANSMIT frequency only. Wheel, ± or arrow keys to adjust (Ctrl = 100 Hz). The offset is kept when you switch it off.',
|
||||
'flxp.smartsdrRemote': 'SmartSDR remote control', 'flxp.offline': 'OFFLINE', 'flxp.waiting': 'Waiting for the FlexRadio… (set CAT to FlexRadio and connect)', 'flxp.transmit': 'Transmit', 'flxp.rfPower': 'RF Power', 'flxp.tunePwr': 'Tune Pwr', 'flxp.splitHint': 'Split: RX/TX on separate slices. ON creates a TX slice +1 kHz (CW) / +5 kHz (SSB) up, like SmartSDR.', 'flxp.sliceHint': 'Click to make this the active slice — frequency, mode, DSP and spot-clicks all follow it.', 'flxp.txSlice': 'This slice transmits', 'flxp.setTxSlice': 'Move TX to this slice (transmit here)', 'flxp.voxDly': 'VOX Dly', 'flxp.speed': 'Speed', 'flxp.pitch': 'Pitch', 'flxp.delay': 'Delay',
|
||||
'flxp.receiveActive': 'Receive (active slice)', 'flxp.muted': 'Muted — click to unmute', 'flxp.mute': 'Mute RX audio', 'flxp.filter': 'Filter', 'flxp.amplifier': 'Amplifier', 'flxp.ampInLine': 'Amplifier is in line (transmitting through PA).', 'flxp.ampBypassed': 'Amplifier bypassed (standby).', 'flxp.pgConnected': 'PowerGenius connected', 'flxp.pgOffline': 'PowerGenius offline', 'flxp.fan': 'Fan', 'flxp.fanStandard': 'Standard', 'flxp.fanContest': 'Contest', 'flxp.fanBroadcast': 'Broadcast', 'flxp.fault': 'FAULT', 'flxp.meters': 'Meters', 'flxp.voltage': 'VOLTAGE', 'flxp.paTemp': 'PA TEMP', 'flxp.txFilter': 'TX filter', 'flxp.micProfile': 'Mic profile', 'flxp.noMeters': "No meters yet — waiting for the radio's UDP stream…", 'flxp.amplifierHdr': 'AMPLIFIER', 'flxp.outputPower': 'OUTPUT POWER', 'flxp.speOffline': 'SPE offline',
|
||||
'flxp.receiveActive': 'Receive (active slice)', 'flxp.muted': 'Muted — click to unmute', 'flxp.mute': 'Mute RX audio', 'flxp.filter': 'Filter', 'flxp.amplifier': 'Amplifier', 'flxp.ampInLine': 'Amplifier is in line (transmitting through PA).', 'flxp.ampBypassed': 'Amplifier bypassed (standby).', 'flxp.pgConnected': 'PowerGenius connected', 'flxp.pgOffline': 'PowerGenius offline', 'flxp.fan': 'Fan', 'flxp.fanStandard': 'Standard', 'flxp.fanContest': 'Contest', 'flxp.fanBroadcast': 'Broadcast', 'flxp.fault': 'FAULT', 'flxp.meters': 'Meters', 'flxp.voltage': 'VOLTAGE', 'flxp.paTemp': 'PA TEMP', 'flxp.txFilter': 'TX filter', 'flxp.micProfile': 'Mic profile', 'flxp.noMeters': "No meters yet — waiting for the radio's UDP stream…", 'flxp.amplifierHdr': 'AMPLIFIER', 'flxp.outputPower': 'OUTPUT POWER', 'flxp.speOffline': 'SPE offline', 'flxp.acomOffline': 'ACOM offline',
|
||||
'icmp.spectrum': 'Spectrum', 'icmp.scopeFixed': 'Fixed — double-click / wheel to tune', 'icmp.scopeCenter': 'Center — follows VFO', 'icmp.scopeOff': 'Scope off', 'icmp.scopePanDown': 'Shift scope −50 kHz', 'icmp.scopePanUp': 'Shift scope +50 kHz', 'icmp.scopeCenterVfo': 'Center scope on the current frequency (±50 kHz)', 'icmp.notConnected': "Icom not connected. Enable the Icom CI-V backend in Settings → CAT and connect the radio's USB port.", 'icmp.refresh': 'Refresh', 'icmp.meters': 'Meters', 'icmp.transmit': 'Transmit', 'icmp.power': 'Power', 'icmp.mic': 'Mic', 'icmp.receive': 'Receive', 'icmp.preamp': 'Preamp', 'icmp.filter': 'Filter', 'icmp.noiseNotch': 'Noise / Notch', 'icmp.autoNotch': 'Auto notch filter', 'icmp.apf': 'Audio peak filter (CW)', 'icmp.clarifiers': 'RIT / ΔTX', 'icmp.ritHint': 'Wheel or ± to shift · Ctrl+←/→ shifts RIT when active', 'icmp.bandsAntenna': 'Bands & Antenna', 'icmp.antenna': 'Antenna', 'icmp.passband': 'Passband / Notch', 'icmp.pbtCenter': 'Center PBT', 'icmp.manualNotch': 'Manual notch — MN on, then set position', 'icmp.squelch': 'Squelch', 'icmp.powerOnHint': 'Power the radio ON (boots ~15 s)', 'icmp.powerOffHint': 'Power the radio OFF', 'icmp.powerOffConfirm': 'Switch the radio OFF?',
|
||||
'rst.clickToFill': 'Click to set RST tx from the signal',
|
||||
'qrz.openTitle': 'Open {call} on QRZ.com',
|
||||
@@ -407,8 +405,6 @@ const fr: Dict = {
|
||||
'settings.theme': 'Thème', 'settings.themeHint': "Thème de couleur de l'interface.",
|
||||
'settings.telemetry': "Envoyer des statistiques d'usage anonymes",
|
||||
'settings.telemetryHint': "ID d'installation + version + OS, une fois par jour — aucun indicatif ni donnée QSO",
|
||||
'settings.liveStatus': 'Publier le statut opérateur en direct',
|
||||
'settings.liveStatusHint': 'multi-op sur MySQL partagé — alimente une page live QRZ',
|
||||
'settings.mainView': 'Vue principale',
|
||||
'settings.mainViewHint': "Choisis ce que l'onglet Principal affiche de chaque côté (par profil).",
|
||||
'settings.leftPane': 'Volet gauche', 'settings.rightPane': 'Volet droit',
|
||||
@@ -581,7 +577,7 @@ const fr: Dict = {
|
||||
'agp.portDeselect': 'Port {letter} — clic pour désélectionner', 'agp.portSelect': 'Sélectionner sur le port {letter}', 'agp.online': 'en ligne', 'agp.offline': 'hors ligne', 'agp.close': 'Fermer', 'agp.connecting': 'Connexion…', 'agp.noAntennas': 'Aucune antenne configurée.', 'agp.filterOnHint': 'Antennes du {band} uniquement — clic pour afficher toutes les bandes', 'agp.filterOffHint': 'Toutes les antennes affichées — clic pour n’afficher que la bande courante',
|
||||
'flxp.ritHint': "RIT — décale uniquement ta fréquence de RÉCEPTION. Molette, ± ou flèches pour ajuster (Ctrl = 100 Hz). Le décalage est conservé quand tu l'éteins.", 'flxp.xitHint': "XIT — décale uniquement ta fréquence d'ÉMISSION. Molette, ± ou flèches pour ajuster (Ctrl = 100 Hz). Le décalage est conservé quand tu l'éteins.",
|
||||
'flxp.smartsdrRemote': 'Contrôle à distance SmartSDR', 'flxp.offline': 'HORS LIGNE', 'flxp.waiting': 'En attente du FlexRadio… (règle le CAT sur FlexRadio et connecte)', 'flxp.transmit': 'Émission', 'flxp.rfPower': 'Puissance RF', 'flxp.tunePwr': 'Puiss. TUNE', 'flxp.splitHint': 'Split : RX/TX sur des slices séparées. ON crée une slice TX +1 kHz (CW) / +5 kHz (SSB) au-dessus, comme SmartSDR.', 'flxp.sliceHint': 'Cliquer pour rendre cette slice active — fréquence, mode, DSP et clics de spot la suivent tous.', 'flxp.txSlice': 'Cette slice émet', 'flxp.setTxSlice': 'Déplacer le TX sur cette slice (émettre ici)', 'flxp.voxDly': 'Délai VOX', 'flxp.speed': 'Vitesse', 'flxp.pitch': 'Tonalité', 'flxp.delay': 'Délai',
|
||||
'flxp.receiveActive': 'Réception (slice active)', 'flxp.muted': 'Coupé — clic pour rétablir', 'flxp.mute': "Couper l'audio RX", 'flxp.filter': 'Filtre', 'flxp.amplifier': 'Amplificateur', 'flxp.ampInLine': 'Amplificateur en ligne (émission via le PA).', 'flxp.ampBypassed': 'Amplificateur en bypass (standby).', 'flxp.pgConnected': 'PowerGenius connecté', 'flxp.pgOffline': 'PowerGenius hors ligne', 'flxp.fan': 'Ventilo', 'flxp.fanStandard': 'Standard', 'flxp.fanContest': 'Contest', 'flxp.fanBroadcast': 'Diffusion', 'flxp.fault': 'DÉFAUT', 'flxp.meters': 'Mesures', 'flxp.voltage': 'TENSION', 'flxp.paTemp': 'TEMP PA', 'flxp.txFilter': 'Filtre TX', 'flxp.micProfile': 'Profil micro', 'flxp.noMeters': 'Aucune mesure — en attente du flux UDP de la radio…', 'flxp.amplifierHdr': 'AMPLIFICATEUR', 'flxp.outputPower': 'PUISSANCE DE SORTIE', 'flxp.speOffline': 'SPE hors ligne',
|
||||
'flxp.receiveActive': 'Réception (slice active)', 'flxp.muted': 'Coupé — clic pour rétablir', 'flxp.mute': "Couper l'audio RX", 'flxp.filter': 'Filtre', 'flxp.amplifier': 'Amplificateur', 'flxp.ampInLine': 'Amplificateur en ligne (émission via le PA).', 'flxp.ampBypassed': 'Amplificateur en bypass (standby).', 'flxp.pgConnected': 'PowerGenius connecté', 'flxp.pgOffline': 'PowerGenius hors ligne', 'flxp.fan': 'Ventilo', 'flxp.fanStandard': 'Standard', 'flxp.fanContest': 'Contest', 'flxp.fanBroadcast': 'Diffusion', 'flxp.fault': 'DÉFAUT', 'flxp.meters': 'Mesures', 'flxp.voltage': 'TENSION', 'flxp.paTemp': 'TEMP PA', 'flxp.txFilter': 'Filtre TX', 'flxp.micProfile': 'Profil micro', 'flxp.noMeters': 'Aucune mesure — en attente du flux UDP de la radio…', 'flxp.amplifierHdr': 'AMPLIFICATEUR', 'flxp.outputPower': 'PUISSANCE DE SORTIE', 'flxp.speOffline': 'SPE hors ligne', 'flxp.acomOffline': 'ACOM hors ligne',
|
||||
'icmp.spectrum': 'Spectre', 'icmp.scopeFixed': 'Fixe — double-clic / molette pour accorder', 'icmp.scopeCenter': 'Centré — suit le VFO', 'icmp.scopeOff': 'Scope éteint', 'icmp.scopePanDown': 'Décaler le scope −50 kHz', 'icmp.scopePanUp': 'Décaler le scope +50 kHz', 'icmp.scopeCenterVfo': 'Centrer le scope sur la fréquence actuelle (±50 kHz)', 'icmp.notConnected': 'Icom non connecté. Active le backend CI-V Icom dans Réglages → CAT et connecte le port USB de la radio.', 'icmp.refresh': 'Rafraîchir', 'icmp.meters': 'Mesures', 'icmp.transmit': 'Émission', 'icmp.power': 'Puissance', 'icmp.mic': 'Micro', 'icmp.receive': 'Réception', 'icmp.preamp': 'Préampli', 'icmp.filter': 'Filtre', 'icmp.noiseNotch': 'Bruit / Notch', 'icmp.autoNotch': 'Filtre notch auto', 'icmp.apf': 'Filtre de pic audio (CW)', 'icmp.clarifiers': 'RIT / ΔTX', 'icmp.ritHint': 'Molette ou ± pour décaler · Ctrl+←/→ décale le RIT si actif', 'icmp.bandsAntenna': 'Bandes & Antenne', 'icmp.antenna': 'Antenne', 'icmp.passband': 'Passe-bande / Notch', 'icmp.pbtCenter': 'Centrer PBT', 'icmp.manualNotch': 'Notch manuel — active MN, puis règle la position', 'icmp.squelch': 'Squelch', 'icmp.powerOnHint': 'Allumer la radio (démarre en ~15 s)', 'icmp.powerOffHint': 'Éteindre la radio', 'icmp.powerOffConfirm': 'Éteindre la radio ?',
|
||||
'rst.clickToFill': 'Clic pour remplir le RST tx depuis le signal',
|
||||
'qrz.openTitle': 'Ouvrir {call} sur QRZ.com',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Single source of truth for the app version shown in the UI (header + About).
|
||||
// Bump this on a release (the release script updates it alongside telemetry.go).
|
||||
export const APP_VERSION = '0.20.6';
|
||||
export const APP_VERSION = '0.20.7';
|
||||
|
||||
// Author / credits, shown in Help -> About.
|
||||
export const APP_AUTHOR = 'F4BPO';
|
||||
|
||||
Reference in New Issue
Block a user