fix: fix a bug adjusting the length of Ultrabeam
This commit is contained in:
@@ -179,7 +179,7 @@ const makeColCatalog = (t: TFn): ColEntry[] => [
|
|||||||
if (s?.new_pota) badges.push(tok('success', t('clg2.newPota')));
|
if (s?.new_pota) badges.push(tok('success', t('clg2.newPota')));
|
||||||
if (badges.length === 0) return <span style={{ color: 'var(--muted-foreground)', fontSize: 10 }}>—</span>;
|
if (badges.length === 0) return <span style={{ color: 'var(--muted-foreground)', fontSize: 10 }}>—</span>;
|
||||||
return (
|
return (
|
||||||
<span style={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center' }}>
|
<span style={{ display: 'flex', height: '100%', flexWrap: 'wrap', gap: 2, alignItems: 'center' }}>
|
||||||
{badges.map((bd, i) => (
|
{badges.map((bd, i) => (
|
||||||
<span key={i} style={{
|
<span key={i} style={{
|
||||||
display: 'inline-flex', alignItems: 'center', height: 15, lineHeight: 1,
|
display: 'inline-flex', alignItems: 'center', height: 15, lineHeight: 1,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { QSOEditModal } from '@/components/QSOEditModal';
|
import { QSOEditModal } from '@/components/QSOEditModal';
|
||||||
|
import { RecentQSOsGrid } from '@/components/RecentQSOsGrid';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
import type { QSOForm } from '@/types';
|
import type { QSOForm } from '@/types';
|
||||||
import {
|
import {
|
||||||
@@ -59,13 +60,13 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) {
|
|||||||
|
|
||||||
// Full-QSO edit modal for an on-air draft (same one Recent QSOs uses).
|
// Full-QSO edit modal for an on-air draft (same one Recent QSOs uses).
|
||||||
const [editingDraft, setEditingDraft] = useState<QSOForm | null>(null);
|
const [editingDraft, setEditingDraft] = useState<QSOForm | null>(null);
|
||||||
|
const [selectedActiveIds, setSelectedActiveIds] = useState<number[]>([]);
|
||||||
|
|
||||||
// Add/edit-contact dialog.
|
// Add/edit-contact dialog.
|
||||||
const [contactOpen, setContactOpen] = useState(false);
|
const [contactOpen, setContactOpen] = useState(false);
|
||||||
const [contact, setContact] = useState<Station>(emptyStation());
|
const [contact, setContact] = useState<Station>(emptyStation());
|
||||||
const [looking, setLooking] = useState(false);
|
const [looking, setLooking] = useState(false);
|
||||||
|
|
||||||
const activeGrid = useRef<any>(null);
|
|
||||||
const rosterGrid = useRef<any>(null);
|
const rosterGrid = useRef<any>(null);
|
||||||
|
|
||||||
// Worked-before for the clicked station (see if/when we contacted it before).
|
// Worked-before for the clicked station (see if/when we contacted it before).
|
||||||
@@ -230,18 +231,6 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) {
|
|||||||
} catch (e: any) { setError(String(e?.message ?? e)); }
|
} catch (e: any) { setError(String(e?.message ?? e)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeCols = useMemo<ColDef<QSOForm>[]>(() => [
|
|
||||||
{ headerName: t('ncp.colCallsign'), field: 'callsign', width: 110, cellClass: 'font-mono font-semibold' },
|
|
||||||
{ headerName: t('ncp.colName'), field: 'name', flex: 1 },
|
|
||||||
{ headerName: 'QTH', field: 'qth', flex: 1 },
|
|
||||||
{ headerName: t('ncp.colTimeOn'), valueGetter: (p) => fmtTimeOn((p.data as any)?.qso_date), width: 90, cellClass: 'font-mono text-[11px]' },
|
|
||||||
{ headerName: t('ncp.colBand'), field: 'band', width: 70, cellClass: 'font-mono' },
|
|
||||||
{ headerName: t('ncp.colMode'), field: 'mode', width: 70, cellClass: 'font-mono' },
|
|
||||||
{ headerName: 'RST S', field: 'rst_sent', width: 70, cellClass: 'font-mono' },
|
|
||||||
{ headerName: 'RST R', field: 'rst_rcvd', width: 70, cellClass: 'font-mono' },
|
|
||||||
{ headerName: t('ncp.colComment'), field: 'comment', flex: 1.5 },
|
|
||||||
], [t]);
|
|
||||||
|
|
||||||
const rosterCols = useMemo<ColDef<Station>[]>(() => [
|
const rosterCols = useMemo<ColDef<Station>[]>(() => [
|
||||||
{ headerName: t('ncp.colCallsign'), field: 'callsign', width: 110, cellClass: 'font-mono font-semibold' },
|
{ headerName: t('ncp.colCallsign'), field: 'callsign', width: 110, cellClass: 'font-mono font-semibold' },
|
||||||
{ headerName: t('ncp.colName'), field: 'name', flex: 1 },
|
{ headerName: t('ncp.colName'), field: 'name', flex: 1 },
|
||||||
@@ -292,26 +281,20 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) {
|
|||||||
{t('ncp.onAirActive')}
|
{t('ncp.onAirActive')}
|
||||||
<span className="ml-auto font-normal normal-case opacity-90">{t('ncp.activeHint')}</span>
|
<span className="ml-auto font-normal normal-case opacity-90">{t('ncp.activeHint')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, minHeight: 0, position: 'relative' }}>
|
<div className="flex flex-col min-h-0 flex-1">
|
||||||
<div style={{ position: 'absolute', inset: 0 }}>
|
<RecentQSOsGrid
|
||||||
<AgGridReact<QSOForm>
|
rows={active}
|
||||||
ref={activeGrid}
|
total={active.length}
|
||||||
theme={hamlogTheme}
|
storageKey="net.onair"
|
||||||
rowData={active}
|
onRowClicked={(q) => showWorkedBefore(q.callsign, (q as any).dxcc ?? 0)}
|
||||||
columnDefs={activeCols}
|
onRowDoubleClicked={(q) => setEditingDraft(q)}
|
||||||
defaultColDef={defaultColDef}
|
onRowSelected={setSelectedActiveIds}
|
||||||
onRowClicked={(e) => e.data && showWorkedBefore(e.data.callsign, (e.data as any).dxcc ?? 0)}
|
/>
|
||||||
onRowDoubleClicked={(e) => e.data && setEditingDraft(e.data)}
|
|
||||||
rowSelection={{ mode: 'singleRow', checkboxes: false, enableClickSelection: true }}
|
|
||||||
animateRows={false}
|
|
||||||
getRowId={(p) => String((p.data as any).id)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{isOpen && active.length > 0 && (
|
{isOpen && active.length > 0 && (
|
||||||
<div className="px-3 py-1.5 border-t border-border/60 bg-muted/30 flex items-center gap-2">
|
<div className="px-3 py-1.5 border-t border-border/60 bg-muted/30 flex items-center gap-2">
|
||||||
<Button variant="ghost" size="sm" className="h-7 text-[11px]"
|
<Button variant="ghost" size="sm" className="h-7 text-[11px]"
|
||||||
onClick={() => { const r = activeGrid.current?.api?.getSelectedRows?.()?.[0] as QSOForm; if (r) deactivate(r.id as number); }}>
|
onClick={() => { if (selectedActiveIds[0] != null) deactivate(selectedActiveIds[0]); }}>
|
||||||
<MinusCircle className="size-3.5" /> {t('ncp.logEndSelected')}
|
<MinusCircle className="size-3.5" /> {t('ncp.logEndSelected')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -335,7 +318,7 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-h-0 overflow-auto">
|
<div className="flex-1 min-h-0 flex flex-col">
|
||||||
{!wbCall ? (
|
{!wbCall ? (
|
||||||
<div className="flex h-full items-center justify-center text-xs text-muted-foreground">{t('ncp.wbHint')}</div>
|
<div className="flex h-full items-center justify-center text-xs text-muted-foreground">{t('ncp.wbHint')}</div>
|
||||||
) : wbBusy ? (
|
) : wbBusy ? (
|
||||||
@@ -343,30 +326,11 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) {
|
|||||||
) : !wb || wb.count === 0 ? (
|
) : !wb || wb.count === 0 ? (
|
||||||
<div className="flex h-full items-center justify-center text-xs text-muted-foreground">{t('ncp.wbNone')} {wbCall}</div>
|
<div className="flex h-full items-center justify-center text-xs text-muted-foreground">{t('ncp.wbNone')} {wbCall}</div>
|
||||||
) : (
|
) : (
|
||||||
<table className="w-full text-xs">
|
<RecentQSOsGrid
|
||||||
<thead className="sticky top-0 bg-muted/30 text-muted-foreground">
|
rows={(wb.entries ?? []) as any}
|
||||||
<tr className="text-left">
|
total={wb.count}
|
||||||
<th className="px-3 py-1 font-semibold">{t('ncp.colDate')}</th>
|
storageKey="net.wb"
|
||||||
<th className="px-2 py-1 font-semibold">{t('ncp.colTimeOn')}</th>
|
/>
|
||||||
<th className="px-2 py-1 font-semibold">{t('ncp.colBand')}</th>
|
|
||||||
<th className="px-2 py-1 font-semibold">{t('ncp.colMode')}</th>
|
|
||||||
<th className="px-2 py-1 font-semibold">RST</th>
|
|
||||||
<th className="px-2 py-1 font-semibold">{t('ncp.colComment')}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{((wb.entries ?? []) as any[]).map((q, i) => (
|
|
||||||
<tr key={q.id ?? i} className="border-t border-border/20 hover:bg-muted/30">
|
|
||||||
<td className="px-3 py-1 font-mono">{String(q.qso_date ?? '').slice(0, 10)}</td>
|
|
||||||
<td className="px-2 py-1 font-mono">{String(q.time_on ?? '').slice(0, 5)}</td>
|
|
||||||
<td className="px-2 py-1">{q.band}</td>
|
|
||||||
<td className="px-2 py-1">{q.mode}</td>
|
|
||||||
<td className="px-2 py-1 font-mono">{(q.rst_sent ?? '')}/{(q.rst_rcvd ?? '')}</td>
|
|
||||||
<td className="px-2 py-1 truncate max-w-[360px]">{q.comment}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,7 +30,12 @@ type Props = {
|
|||||||
// Bump this number to programmatically select every row (e.g. after a search
|
// Bump this number to programmatically select every row (e.g. after a search
|
||||||
// in the QSL Manager, where the default is "all selected").
|
// in the QSL Manager, where the default is "all selected").
|
||||||
selectAllSignal?: number;
|
selectAllSignal?: number;
|
||||||
|
// storageKey scopes the persisted column layout/visibility. Omit for the main
|
||||||
|
// log (keeps the historical keys); pass a distinct value (e.g. "net.onair") to
|
||||||
|
// reuse this grid elsewhere with its OWN independent column config.
|
||||||
|
storageKey?: string;
|
||||||
onRowDoubleClicked?: (q: QSOForm) => void;
|
onRowDoubleClicked?: (q: QSOForm) => void;
|
||||||
|
onRowClicked?: (q: QSOForm) => void;
|
||||||
onRowSelected?: (ids: number[]) => void;
|
onRowSelected?: (ids: number[]) => void;
|
||||||
onUpdateFromCty?: (ids: number[]) => void;
|
onUpdateFromCty?: (ids: number[]) => void;
|
||||||
onUpdateFromQRZ?: (ids: number[]) => void;
|
onUpdateFromQRZ?: (ids: number[]) => void;
|
||||||
@@ -49,7 +54,7 @@ type Props = {
|
|||||||
awardCols?: { code: string; name: string }[];
|
awardCols?: { code: string; name: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const COL_STATE_KEY = 'hamlog.qsoColState.v2';
|
const BASE_COLSTATE_KEY = 'hamlog.qsoColState.v2';
|
||||||
|
|
||||||
function fmtMhzDots(hz?: number): string {
|
function fmtMhzDots(hz?: number): string {
|
||||||
if (!hz) return '';
|
if (!hz) return '';
|
||||||
@@ -240,10 +245,13 @@ export const groupLabel = (t: TFn, g: string): string => t(GRP_KEYS[g] ?? g);
|
|||||||
const stripAwardCols = (st: any[] | null | undefined): any[] =>
|
const stripAwardCols = (st: any[] | null | undefined): any[] =>
|
||||||
(st ?? []).filter((s) => !String(s?.colId ?? '').startsWith('award_'));
|
(st ?? []).filter((s) => !String(s?.colId ?? '').startsWith('award_'));
|
||||||
|
|
||||||
export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRowSelected, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL, onBulkEdit, onExportSelected, onExportFiltered, onExportCabrilloSelected, onExportCabrilloFiltered, onDelete, awardCols }: Props) {
|
export function RecentQSOsGrid({ rows, selectAllSignal, storageKey, onRowDoubleClicked, onRowClicked, onRowSelected, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL, onBulkEdit, onExportSelected, onExportFiltered, onExportCabrilloSelected, onExportCabrilloFiltered, onDelete, awardCols }: Props) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const gridRef = useRef<any>(null);
|
const gridRef = useRef<any>(null);
|
||||||
const [pickerOpen, setPickerOpen] = useState(false);
|
const [pickerOpen, setPickerOpen] = useState(false);
|
||||||
|
// Column-layout persistence keys — scoped by storageKey so a reused instance
|
||||||
|
// (e.g. the Net panel) keeps its own layout independent of the main log.
|
||||||
|
const colStateKey = storageKey ? `hamlog.qsoColState.${storageKey}` : BASE_COLSTATE_KEY;
|
||||||
const [menu, setMenu] = useState<QSOMenuState>(null);
|
const [menu, setMenu] = useState<QSOMenuState>(null);
|
||||||
|
|
||||||
// Localized column catalog — rebuilt when the language changes.
|
// Localized column catalog — rebuilt when the language changes.
|
||||||
@@ -283,7 +291,7 @@ export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRo
|
|||||||
// across a columnDefs rebuild instead of re-reading `hide` — which is exactly
|
// across a columnDefs rebuild instead of re-reading `hide` — which is exactly
|
||||||
// why previously-shown award columns kept vanishing on reopen. A dedicated set
|
// why previously-shown award columns kept vanishing on reopen. A dedicated set
|
||||||
// is deterministic: it drives `hide` directly and is re-enforced below.
|
// is deterministic: it drives `hide` directly and is re-enforced below.
|
||||||
const AWARD_SHOWN_KEY = 'hamlog.awardColsShown';
|
const AWARD_SHOWN_KEY = storageKey ? `hamlog.awardColsShown.${storageKey}` : 'hamlog.awardColsShown';
|
||||||
const [awardShown, setAwardShown] = useState<Set<string>>(() => new Set((loadLocal(AWARD_SHOWN_KEY) ?? []).map((s) => String(s).toUpperCase())));
|
const [awardShown, setAwardShown] = useState<Set<string>>(() => new Set((loadLocal(AWARD_SHOWN_KEY) ?? []).map((s) => String(s).toUpperCase())));
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fresh machine: hydrate the set from the portable DB copy, then seed the cache.
|
// Fresh machine: hydrate the set from the portable DB copy, then seed the cache.
|
||||||
@@ -341,21 +349,21 @@ export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRo
|
|||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
function onGridReady(e: GridReadyEvent) {
|
function onGridReady(e: GridReadyEvent) {
|
||||||
const local = loadLocal(COL_STATE_KEY);
|
const local = loadLocal(colStateKey);
|
||||||
if (local) e.api.applyColumnState({ state: stripAwardCols(local) as ColumnState[], applyOrder: true });
|
if (local) e.api.applyColumnState({ state: stripAwardCols(local) as ColumnState[], applyOrder: true });
|
||||||
// Fall back to the portable DB copy when the local cache is empty
|
// Fall back to the portable DB copy when the local cache is empty
|
||||||
// (fresh machine / after a reinstall), then re-seed the cache.
|
// (fresh machine / after a reinstall), then re-seed the cache.
|
||||||
loadRemote(COL_STATE_KEY).then((remote) => {
|
loadRemote(colStateKey).then((remote) => {
|
||||||
if (remote && !local) {
|
if (remote && !local) {
|
||||||
e.api.applyColumnState({ state: stripAwardCols(remote) as ColumnState[], applyOrder: true });
|
e.api.applyColumnState({ state: stripAwardCols(remote) as ColumnState[], applyOrder: true });
|
||||||
seedLocal(COL_STATE_KEY, remote);
|
seedLocal(colStateKey, remote);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const saveColumnState = useCallback(() => {
|
const saveColumnState = useCallback(() => {
|
||||||
if (restoringRef.current) return; // ignore the events fired by a column rebuild
|
if (restoringRef.current) return; // ignore the events fired by a column rebuild
|
||||||
const state = gridRef.current?.api?.getColumnState();
|
const state = gridRef.current?.api?.getColumnState();
|
||||||
if (state) saveState(COL_STATE_KEY, stripAwardCols(state));
|
if (state) saveState(colStateKey, stripAwardCols(state));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// The award columns load asynchronously; when they arrive (or change) the
|
// The award columns load asynchronously; when they arrive (or change) the
|
||||||
@@ -365,7 +373,7 @@ export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRo
|
|||||||
// every rebuild so the user's choices win. No-op before the grid is ready.
|
// every rebuild so the user's choices win. No-op before the grid is ready.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const api = gridRef.current?.api;
|
const api = gridRef.current?.api;
|
||||||
const local = loadLocal(COL_STATE_KEY);
|
const local = loadLocal(colStateKey);
|
||||||
if (api && local) api.applyColumnState({ state: stripAwardCols(local) as ColumnState[], applyOrder: true });
|
if (api && local) api.applyColumnState({ state: stripAwardCols(local) as ColumnState[], applyOrder: true });
|
||||||
// Re-enable saving once AG Grid has settled the column events from the rebuild.
|
// Re-enable saving once AG Grid has settled the column events from the rebuild.
|
||||||
const t = window.setTimeout(() => { restoringRef.current = false; }, 0);
|
const t = window.setTimeout(() => { restoringRef.current = false; }, 0);
|
||||||
@@ -465,6 +473,7 @@ export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRo
|
|||||||
onColumnVisible={saveColumnState}
|
onColumnVisible={saveColumnState}
|
||||||
onSortChanged={saveColumnState}
|
onSortChanged={saveColumnState}
|
||||||
onRowDoubleClicked={handleRowDoubleClicked}
|
onRowDoubleClicked={handleRowDoubleClicked}
|
||||||
|
onRowClicked={(e) => e.data && onRowClicked?.(e.data)}
|
||||||
onSelectionChanged={onSelectionChanged}
|
onSelectionChanged={onSelectionChanged}
|
||||||
onCellContextMenu={onCellContextMenu}
|
onCellContextMenu={onCellContextMenu}
|
||||||
preventDefaultOnContextMenu
|
preventDefaultOnContextMenu
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ function MotorAntennaWidget({ ant, refetch, t }: { ant: AntStatus; refetch: () =
|
|||||||
const [lengths, setLengths] = useState<number[]>([]); // current element lengths (mm), from ReadElements
|
const [lengths, setLengths] = useState<number[]>([]); // current element lengths (mm), from ReadElements
|
||||||
const [reading, setReading] = useState(false);
|
const [reading, setReading] = useState(false);
|
||||||
const [busyEl, setBusyEl] = useState<number | null>(null);
|
const [busyEl, setBusyEl] = useState<number | null>(null);
|
||||||
|
const [editingEl, setEditingEl] = useState<number | null>(null); // element whose mm is being typed
|
||||||
|
const [editVal, setEditVal] = useState('');
|
||||||
const run = (p: Promise<any>) => p.then(refetch).catch((e) => setErr(String(e?.message ?? e)));
|
const run = (p: Promise<any>) => p.then(refetch).catch((e) => setErr(String(e?.message ?? e)));
|
||||||
const isUB = ant.type !== 'steppir';
|
const isUB = ant.type !== 'steppir';
|
||||||
|
|
||||||
@@ -125,16 +127,24 @@ function MotorAntennaWidget({ ant, refetch, t }: { ant: AntStatus; refetch: () =
|
|||||||
// +/- starts from the real values rather than blind.
|
// +/- starts from the real values rather than blind.
|
||||||
useEffect(() => { if (isUB && ant.connected) readLengths(); }, [isUB, ant.connected, readLengths]);
|
useEffect(() => { if (isUB && ant.connected) readLengths(); }, [isUB, ant.connected, readLengths]);
|
||||||
|
|
||||||
// Nudge one element by ±2 mm from its current known length.
|
// Send an absolute length to one element and remember it as the new baseline.
|
||||||
const nudge = async (i: number, delta: number) => {
|
const setLen = async (i: number, mm: number) => {
|
||||||
const cur = lengths[i] ?? 0;
|
const next = Math.max(0, Math.round(mm));
|
||||||
const next = Math.max(0, cur + delta);
|
|
||||||
setBusyEl(i); setErr('');
|
setBusyEl(i); setErr('');
|
||||||
setLengths((ls) => { const c = [...ls]; c[i] = next; return c; }); // optimistic
|
setLengths((ls) => { const c = [...ls]; c[i] = next; return c; }); // optimistic
|
||||||
try { await MotorSetElement(i, next); refetch(); }
|
try { await MotorSetElement(i, next); refetch(); }
|
||||||
catch (e: any) { setErr(String(e?.message ?? e)); }
|
catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||||
finally { setBusyEl(null); }
|
finally { setBusyEl(null); }
|
||||||
};
|
};
|
||||||
|
// Nudge one element by ±2 mm from its current known length.
|
||||||
|
const nudge = (i: number, delta: number) => setLen(i, (lengths[i] ?? 0) + delta);
|
||||||
|
// Commit a typed exact length (click on the mm value). Lets the operator fix
|
||||||
|
// the baseline when the auto-read is off, so +/- then work reliably.
|
||||||
|
const commitEdit = (i: number) => {
|
||||||
|
const v = parseInt(editVal, 10);
|
||||||
|
setEditingEl(null);
|
||||||
|
if (!isNaN(v) && v >= 0 && v !== lengths[i]) setLen(i, v);
|
||||||
|
};
|
||||||
const dirs: [number, string][] = [[0, 'N'], [1, '180°'], [2, t('station.bi')]];
|
const dirs: [number, string][] = [[0, 'N'], [1, '180°'], [2, t('station.bi')]];
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-border bg-card shadow-sm overflow-hidden h-full">
|
<div className="rounded-xl border border-border bg-card shadow-sm overflow-hidden h-full">
|
||||||
@@ -191,9 +201,19 @@ function MotorAntennaWidget({ ant, refetch, t }: { ant: AntStatus; refetch: () =
|
|||||||
className="flex items-center justify-center size-7 rounded-md border border-border hover:bg-muted disabled:opacity-40 shrink-0">
|
className="flex items-center justify-center size-7 rounded-md border border-border hover:bg-muted disabled:opacity-40 shrink-0">
|
||||||
<Minus className="size-3.5" />
|
<Minus className="size-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<span className="text-sm font-mono font-bold flex-1 min-w-0 text-center tabular-nums">
|
{editingEl === i ? (
|
||||||
{busyEl === i ? <Loader2 className="size-3.5 animate-spin inline" /> : `${mm} mm`}
|
<input autoFocus type="number" value={editVal}
|
||||||
</span>
|
onChange={(e) => setEditVal(e.target.value)}
|
||||||
|
onBlur={() => commitEdit(i)}
|
||||||
|
onKeyDown={(e) => { if (e.key === 'Enter') commitEdit(i); if (e.key === 'Escape') setEditingEl(null); }}
|
||||||
|
className="text-sm font-mono font-bold flex-1 min-w-0 text-center tabular-nums bg-transparent border border-primary rounded px-1" />
|
||||||
|
) : (
|
||||||
|
<span className="text-sm font-mono font-bold flex-1 min-w-0 text-center tabular-nums cursor-pointer hover:underline"
|
||||||
|
title={t('station.setExactLen')}
|
||||||
|
onClick={() => { setEditingEl(i); setEditVal(String(mm)); }}>
|
||||||
|
{busyEl === i ? <Loader2 className="size-3.5 animate-spin inline" /> : `${mm} mm`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<button type="button" disabled={!ant.connected || busyEl !== null}
|
<button type="button" disabled={!ant.connected || busyEl !== null}
|
||||||
onClick={() => nudge(i, ELEMENT_STEP)}
|
onClick={() => nudge(i, ELEMENT_STEP)}
|
||||||
className="flex items-center justify-center size-7 rounded-md border border-border hover:bg-muted disabled:opacity-40 shrink-0">
|
className="flex items-center justify-center size-7 rounded-md border border-border hover:bg-muted disabled:opacity-40 shrink-0">
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ const en: Dict = {
|
|||||||
'uscty.backfillIntro': 'Resolve county (and grid) for US QSOs already in your log that are missing them. Existing values are kept — only blanks are filled.',
|
'uscty.backfillIntro': 'Resolve county (and grid) for US QSOs already in your log that are missing them. Existing values are kept — only blanks are filled.',
|
||||||
'uscty.backfillRun': 'Fill missing counties',
|
'uscty.backfillRun': 'Fill missing counties',
|
||||||
'uscty.backfillDone': '{s} US QSOs scanned · {c} counties, {g} grids filled.',
|
'uscty.backfillDone': '{s} US QSOs scanned · {c} counties, {g} grids filled.',
|
||||||
'station.title': 'Station Control', 'station.rotator': 'Rotator', 'station.rotatorNoRead': 'No heading read', 'station.pattern': 'Pattern', 'station.bi': 'Bi', 'station.retract': 'Retract elements', 'station.moving': 'MOVING', 'station.elements': 'Elements (mm)', 'station.read': 'Read', 'station.readLengths': 'Read current element lengths from the controller', 'station.noLengths': 'Lengths unknown — click Read to fetch them from the controller.', 'station.element': 'Element', 'station.reflector': 'Reflector', 'station.driven': 'Driven', 'station.director': 'Dir', 'station.set': 'Set', 'station.elementsHint': 'Each press lengthens/shortens the element by 2 mm (like the physical console). Verify which element responds on your antenna.', 'station.go': 'Go', 'station.stop': 'Stop', 'station.dragHint': 'Drag a widget by its card to reorder. Pick a column count to lay them out in a grid.', 'station.colsAuto': 'Auto', 'station.addDevice': 'Add device', 'station.editDevice': 'Edit device', 'station.empty': 'No relay boards yet. Add a WebSwitch 1216H or a KMTronic 8-relay board to control your station power and accessories.', 'station.online': 'Online', 'station.offline': 'Offline', 'station.edit': 'Edit', 'station.delete': 'Delete', 'station.relay': 'Relay', 'station.on': 'ON', 'station.off': 'OFF', 'station.type': 'Device type', 'station.name': 'Name', 'station.host': 'Host / IP', 'station.user': 'Username', 'station.pass': 'Password', 'station.optional': 'optional', 'station.labels': 'Relay labels', 'station.cancel': 'Cancel', 'station.save': 'Save',
|
'station.title': 'Station Control', 'station.rotator': 'Rotator', 'station.rotatorNoRead': 'No heading read', 'station.pattern': 'Pattern', 'station.bi': 'Bi', 'station.retract': 'Retract elements', 'station.moving': 'MOVING', 'station.elements': 'Elements (mm)', 'station.read': 'Read', 'station.readLengths': 'Read current element lengths from the controller', 'station.noLengths': 'Lengths unknown — click Read to fetch them from the controller.', 'station.element': 'Element', 'station.reflector': 'Reflector', 'station.driven': 'Driven', 'station.director': 'Dir', 'station.set': 'Set', 'station.elementsHint': 'Each press lengthens/shortens the element by 2 mm (like the physical console). Verify which element responds on your antenna.', 'station.setExactLen': 'Click to type the exact current length (fixes the baseline if the auto-read is off).', 'station.go': 'Go', 'station.stop': 'Stop', 'station.dragHint': 'Drag a widget by its card to reorder. Pick a column count to lay them out in a grid.', 'station.colsAuto': 'Auto', 'station.addDevice': 'Add device', 'station.editDevice': 'Edit device', 'station.empty': 'No relay boards yet. Add a WebSwitch 1216H or a KMTronic 8-relay board to control your station power and accessories.', 'station.online': 'Online', 'station.offline': 'Offline', 'station.edit': 'Edit', 'station.delete': 'Delete', 'station.relay': 'Relay', 'station.on': 'ON', 'station.off': 'OFF', 'station.type': 'Device type', 'station.name': 'Name', 'station.host': 'Host / IP', 'station.user': 'Username', 'station.pass': 'Password', 'station.optional': 'optional', 'station.labels': 'Relay labels', 'station.cancel': 'Cancel', 'station.save': 'Save',
|
||||||
'sec.awards': 'Awards', 'sec.cat': 'CAT interface', 'sec.rotator': 'Rotator', 'sec.winkeyer': 'CW Keyer',
|
'sec.awards': 'Awards', 'sec.cat': 'CAT interface', 'sec.rotator': 'Rotator', 'sec.winkeyer': 'CW Keyer',
|
||||||
'sec.antenna': 'Ultrabeam / Steppir', 'sec.antgenius': 'Antenna Genius', 'sec.pgxl': 'Power Genius', 'sec.flex': 'FlexRadio', 'sec.audio': 'Audio devices',
|
'sec.antenna': 'Ultrabeam / Steppir', 'sec.antgenius': 'Antenna Genius', 'sec.pgxl': 'Power Genius', 'sec.flex': 'FlexRadio', 'sec.audio': 'Audio devices',
|
||||||
// General panel
|
// General panel
|
||||||
@@ -381,7 +381,7 @@ const fr: Dict = {
|
|||||||
'uscty.backfillIntro': "Résout le comté (et le locator) pour les QSO US déjà dans ton log qui n'en ont pas. Les valeurs existantes sont conservées — seuls les vides sont remplis.",
|
'uscty.backfillIntro': "Résout le comté (et le locator) pour les QSO US déjà dans ton log qui n'en ont pas. Les valeurs existantes sont conservées — seuls les vides sont remplis.",
|
||||||
'uscty.backfillRun': 'Remplir les comtés manquants',
|
'uscty.backfillRun': 'Remplir les comtés manquants',
|
||||||
'uscty.backfillDone': '{s} QSO US analysés · {c} comtés, {g} locators remplis.',
|
'uscty.backfillDone': '{s} QSO US analysés · {c} comtés, {g} locators remplis.',
|
||||||
'station.title': 'Contrôle station', 'station.rotator': 'Rotator', 'station.rotatorNoRead': 'Azimut non lu', 'station.pattern': 'Diagramme', 'station.bi': 'Bi', 'station.retract': 'Rétracter les éléments', 'station.moving': 'EN MOUVEMENT', 'station.elements': 'Éléments (mm)', 'station.read': 'Lire', 'station.readLengths': 'Lire les longueurs actuelles depuis le contrôleur', 'station.noLengths': 'Longueurs inconnues — clique sur Lire pour les récupérer depuis le contrôleur.', 'station.element': 'Élément', 'station.reflector': 'Réflecteur', 'station.driven': 'Radiateur', 'station.director': 'Dir', 'station.set': 'Régler', 'station.elementsHint': "Chaque appui allonge/raccourcit l'élément de 2 mm (comme le pupitre). Vérifie quel élément répond sur ton antenne.", 'station.go': 'Aller', 'station.stop': 'Stop', 'station.dragHint': 'Glisse une carte pour réordonner. Choisis un nombre de colonnes pour la disposition.', 'station.colsAuto': 'Auto', 'station.addDevice': 'Ajouter un appareil', 'station.editDevice': "Modifier l'appareil", 'station.empty': "Aucune carte relais. Ajoute un WebSwitch 1216H ou une carte KMTronic 8 relais pour piloter l'alimentation et les accessoires de ta station.", 'station.online': 'En ligne', 'station.offline': 'Hors ligne', 'station.edit': 'Modifier', 'station.delete': 'Supprimer', 'station.relay': 'Relais', 'station.on': 'ON', 'station.off': 'OFF', 'station.type': "Type d'appareil", 'station.name': 'Nom', 'station.host': 'Hôte / IP', 'station.user': "Nom d'utilisateur", 'station.pass': 'Mot de passe', 'station.optional': 'optionnel', 'station.labels': 'Libellés des relais', 'station.cancel': 'Annuler', 'station.save': 'Enregistrer',
|
'station.title': 'Contrôle station', 'station.rotator': 'Rotator', 'station.rotatorNoRead': 'Azimut non lu', 'station.pattern': 'Diagramme', 'station.bi': 'Bi', 'station.retract': 'Rétracter les éléments', 'station.moving': 'EN MOUVEMENT', 'station.elements': 'Éléments (mm)', 'station.read': 'Lire', 'station.readLengths': 'Lire les longueurs actuelles depuis le contrôleur', 'station.noLengths': 'Longueurs inconnues — clique sur Lire pour les récupérer depuis le contrôleur.', 'station.element': 'Élément', 'station.reflector': 'Réflecteur', 'station.driven': 'Radiateur', 'station.director': 'Dir', 'station.set': 'Régler', 'station.elementsHint': "Chaque appui allonge/raccourcit l'élément de 2 mm (comme le pupitre). Vérifie quel élément répond sur ton antenne.", 'station.setExactLen': "Clique pour taper la longueur actuelle exacte (recale la base si la lecture auto est fausse).", 'station.go': 'Aller', 'station.stop': 'Stop', 'station.dragHint': 'Glisse une carte pour réordonner. Choisis un nombre de colonnes pour la disposition.', 'station.colsAuto': 'Auto', 'station.addDevice': 'Ajouter un appareil', 'station.editDevice': "Modifier l'appareil", 'station.empty': "Aucune carte relais. Ajoute un WebSwitch 1216H ou une carte KMTronic 8 relais pour piloter l'alimentation et les accessoires de ta station.", 'station.online': 'En ligne', 'station.offline': 'Hors ligne', 'station.edit': 'Modifier', 'station.delete': 'Supprimer', 'station.relay': 'Relais', 'station.on': 'ON', 'station.off': 'OFF', 'station.type': "Type d'appareil", 'station.name': 'Nom', 'station.host': 'Hôte / IP', 'station.user': "Nom d'utilisateur", 'station.pass': 'Mot de passe', 'station.optional': 'optionnel', 'station.labels': 'Libellés des relais', 'station.cancel': 'Annuler', 'station.save': 'Enregistrer',
|
||||||
'sec.awards': 'Diplômes', 'sec.cat': 'Interface CAT', 'sec.rotator': 'Rotator', 'sec.winkeyer': 'Manipulateur CW',
|
'sec.awards': 'Diplômes', 'sec.cat': 'Interface CAT', 'sec.rotator': 'Rotator', 'sec.winkeyer': 'Manipulateur CW',
|
||||||
'sec.antenna': 'Antenne motorisée', 'sec.antgenius': 'Antenna Genius', 'sec.pgxl': 'Power Genius', 'sec.flex': 'FlexRadio', 'sec.audio': 'Périphériques audio',
|
'sec.antenna': 'Antenne motorisée', 'sec.antgenius': 'Antenna Genius', 'sec.pgxl': 'Power Genius', 'sec.flex': 'FlexRadio', 'sec.audio': 'Périphériques audio',
|
||||||
'gen.hint': 'Comportement de l\'application (enregistré immédiatement).',
|
'gen.hint': 'Comportement de l\'application (enregistré immédiatement).',
|
||||||
|
|||||||
Reference in New Issue
Block a user