Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae8e5b1bc6 | ||
|
|
ad69371f6a | ||
|
|
3f97084246 | ||
|
|
74dfc3a725 |
@@ -8330,8 +8330,15 @@ func (a *App) GetCATSettings() (CATSettings, error) {
|
||||
if n, _ := strconv.Atoi(m[keyCATIcomBaud]); n > 0 {
|
||||
out.IcomBaud = n
|
||||
}
|
||||
if n, _ := strconv.Atoi(m[keyCATIcomAddr]); n > 0 && n <= 0xFF {
|
||||
out.IcomAddr = n
|
||||
// 0x00 is a real CI-V address an operator may need (a bare interface, a rig
|
||||
// left at its factory broadcast address), and "> 0" silently sent them back
|
||||
// to the IC-7610 default with no way to say what they meant. An EMPTY
|
||||
// setting is what means "never configured" — so the error is what decides,
|
||||
// not the value.
|
||||
if v := strings.TrimSpace(m[keyCATIcomAddr]); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n >= 0 && n <= 0xFF {
|
||||
out.IcomAddr = n
|
||||
}
|
||||
}
|
||||
if out.Backend == "" {
|
||||
out.Backend = "omnirig"
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
[
|
||||
{
|
||||
"version": "0.27.8",
|
||||
"date": "",
|
||||
"en": [
|
||||
"The band matrix’s DIG row is now a rotation: click it and it answers for FT8, then FT4, then each digital mode your mode list holds — in YOUR order — then back to DIG. One row per digital mode would be the honest layout and there is no height for it beside the other widgets, so the row keeps its place and changes what it says. The label column is sized once for the longest mode it can show, so the matrix never shifts as the rotation comes round to RTTY.",
|
||||
"Icom console: LSB and USB are separate buttons and can finally be commanded by name — the single SSB button resolved the sideband from the band, so there was no way to ask an IC-7300 for USB on 40 m. A rig reporting the folded “SSB” still lights the side its frequency implies.",
|
||||
"Icom console: a 60 m band button, and the antenna and PSK controls only appear on radios that have them. An IC-7300 has one antenna socket and no native PSK mode, so ANT1/ANT2 could only ever disagree with its front panel and the PSK button was dead furniture.",
|
||||
"Icom console: the mic gain is no longer hidden outside phone modes — on USB-D it still sets what the radio transmits at, and an operator who lives in FT8 had none at all.",
|
||||
"Icom CI-V: address 0x00 can be chosen. It was silently refused and replaced by the IC-7610 default, with no way to say what was meant."
|
||||
],
|
||||
"fr": [
|
||||
"La ligne DIG de la matrice devient une rotation : un clic et elle répond pour FT8, puis FT4, puis chaque mode numérique de votre liste — dans VOTRE ordre — puis retour à DIG. Une ligne par mode numérique serait la mise en page honnête et la hauteur manque à côté des autres widgets : la ligne garde donc sa place et change ce qu’elle dit. La colonne des libellés est dimensionnée une fois pour le plus long mode qu’elle peut afficher : la matrice ne bouge donc plus quand la rotation arrive sur RTTY.",
|
||||
"Console Icom : LSB et USB sont deux boutons distincts et peuvent enfin être demandés par leur nom — le bouton SSB unique déduisait la bande latérale de la fréquence, impossible donc de demander l’USB à un IC-7300 sur 40 m. Une radio qui annonce le « SSB » générique allume malgré tout le côté que sa fréquence implique.",
|
||||
"Console Icom : un bouton de bande 60 m, et les commandes antenne et PSK n’apparaissent que sur les radios qui en disposent. Un IC-7300 n’a qu’une prise d’antenne et pas de mode PSK natif : ANT1/ANT2 ne pouvait que contredire sa face avant, et le bouton PSK était un meuble mort.",
|
||||
"Console Icom : le gain micro n’est plus masqué hors des modes phonie — en USB-D il règle toujours le niveau d’émission, et un opérateur qui vit en FT8 n’en avait aucun.",
|
||||
"CI-V Icom : l’adresse 0x00 peut être choisie. Elle était refusée en silence et remplacée par le défaut IC-7610, sans moyen de dire ce que l’on voulait."
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "0.27.7",
|
||||
"date": "",
|
||||
|
||||
@@ -7411,6 +7411,7 @@ export default function App() {
|
||||
band={band}
|
||||
mode={mode}
|
||||
bands={bands}
|
||||
modes={modes}
|
||||
satellites={satellites}
|
||||
onEditQso={openEdit}
|
||||
{...(!callsign.trim() && selQso ? {
|
||||
|
||||
@@ -15,7 +15,10 @@ interface Props {
|
||||
busy: boolean;
|
||||
currentBand: string;
|
||||
currentMode: string;
|
||||
bands?: string[]; // operator's configured bands; falls back to DEFAULT_BANDS
|
||||
bands?: string[];
|
||||
// The operator's configured mode list, in THEIR order: the digital row
|
||||
// rotates through it.
|
||||
modes?: string[]; // operator's configured bands; falls back to DEFAULT_BANDS
|
||||
hasCall?: boolean; // a callsign is being entered — only then highlight the "current entry" cell
|
||||
// DX station coordinates, for its sunrise/sunset. Optional: many spots resolve
|
||||
// to an entity with no position at all, and the block simply does not appear.
|
||||
@@ -121,10 +124,31 @@ function cellTitle(t: (k: string) => string, band: string, cls: string, status:
|
||||
return `${band} ${cls}: ${desc}${mine ? ' — ' + mine : ''}${current ? ' — ' + t('mx.current') : ''}`;
|
||||
}
|
||||
|
||||
export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCall = true, lat, lon, forCall, onEditQso }: Props) {
|
||||
export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, modes, hasCall = true, lat, lon, forCall, onEditQso }: Props) {
|
||||
const { t } = useI18n();
|
||||
// Cell drill-down: which band+class the operator clicked, or null.
|
||||
const [slot, setSlot] = useState<{ band: string; cls: string } | null>(null);
|
||||
|
||||
// The DIGITAL row is a rotation, not a fixed row.
|
||||
//
|
||||
// One row for every digital mode would be the honest layout and there is no
|
||||
// height for it — the matrix sits in a fixed panel beside a dozen widgets.
|
||||
// So the row keeps its place and changes what it answers: DIG (all of them),
|
||||
// then each digital mode the operator actually uses, in the order their mode
|
||||
// list gives, then back to DIG. The backend publishes the same cells under
|
||||
// both the class name and the raw mode, so a rotation costs no round trip.
|
||||
const digModes = useMemo(
|
||||
() => (modes ?? [])
|
||||
.map((m) => (m || '').toUpperCase().trim())
|
||||
.filter((m) => m !== '' && m !== 'CW' && !PHONE_MODES.has(m)),
|
||||
[modes],
|
||||
);
|
||||
const [digIdx, setDigIdx] = useState(0); // 0 = the DIG group itself
|
||||
// A shorter mode list (the operator edited it) must not strand the rotation
|
||||
// on a row that no longer exists.
|
||||
const digPos = digModes.length ? digIdx % (digModes.length + 1) : 0;
|
||||
const digRow = digPos === 0 ? 'DIG' : digModes[digPos - 1];
|
||||
const cycleDig = () => setDigIdx((i) => (digModes.length ? (i + 1) % (digModes.length + 1) : 0));
|
||||
// Columns from the operator's configured bands (so the matrix shows only the
|
||||
// bands they actually use), falling back to the built-in default set.
|
||||
const cols = useMemo(
|
||||
@@ -310,7 +334,7 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal
|
||||
<table className="border-separate" style={{ borderSpacing: 3 }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-[26px]" />
|
||||
<th className="w-[38px] min-w-[38px] max-w-[38px]" />
|
||||
{cols.map((b) => (
|
||||
<th
|
||||
key={b.tag}
|
||||
@@ -325,13 +349,29 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{CLASSES.map((cls) => {
|
||||
const classCurrent = classMatchesMode(cls, currentMode);
|
||||
{CLASSES.map((clsBase) => {
|
||||
const cls = clsBase === 'DIG' ? digRow : clsBase;
|
||||
// On a specific digital mode the "you are here" mark has to be that
|
||||
// mode, not any digital one — otherwise every FT4 entry lights the
|
||||
// FT8 row it happens to be cycled to.
|
||||
const classCurrent = cls === clsBase
|
||||
? classMatchesMode(cls, currentMode)
|
||||
: (currentMode || '').toUpperCase() === cls;
|
||||
return (
|
||||
<tr key={cls}>
|
||||
<th
|
||||
onClick={clsBase === 'DIG' && digModes.length ? cycleDig : undefined}
|
||||
title={clsBase === 'DIG' && digModes.length ? t('bsg.digCycle') : undefined}
|
||||
className={cn(
|
||||
'font-mono text-[11px] font-semibold pr-1.5 text-right w-[26px]',
|
||||
// Sized once for the LONGEST label the rotation can show,
|
||||
// and pinned there: a column that grows when RTTY comes
|
||||
// round shifts every band beneath it, and the eye reads
|
||||
// that as the matrix moving rather than the row changing.
|
||||
'font-mono font-semibold pr-1.5 text-right w-[38px] min-w-[38px] max-w-[38px] overflow-hidden',
|
||||
// Beyond four characters (PSK31, MSK144) the type gives way
|
||||
// instead of the column.
|
||||
cls.length > 4 ? 'text-[9px]' : 'text-[11px]',
|
||||
clsBase === 'DIG' && digModes.length ? 'cursor-pointer hover:text-foreground' : '',
|
||||
classCurrent ? 'text-primary font-extrabold' : 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -71,6 +71,7 @@ interface Props {
|
||||
band: string;
|
||||
mode: string;
|
||||
bands?: string[]; // configured bands for the worked-before matrix columns
|
||||
modes?: string[]; // configured modes, in order — the matrix cycles its digital row through them
|
||||
// The station's satellites, for the SAT_NAME dropdown. Passed in rather than
|
||||
// read here: the list lives in Preferences, and App already reloads it when
|
||||
// Preferences close — a panel reading it once at mount would need a restart.
|
||||
@@ -155,7 +156,7 @@ function Field({ label, span = 1, className, children }: { label: string; span?:
|
||||
);
|
||||
}
|
||||
|
||||
export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, qth, name, country, comment, note, details, onChange, wb, wbBusy, band, mode, bands, satellites = [], slotCall, slotBand, slotMode, slotWb, slotWbBusy, tab, onTab, keyerActive, onEditQso }: Props) {
|
||||
export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, qth, name, country, comment, note, details, onChange, wb, wbBusy, band, mode, bands, modes, satellites = [], slotCall, slotBand, slotMode, slotWb, slotWbBusy, tab, onTab, keyerActive, onEditQso }: Props) {
|
||||
const { t } = useI18n();
|
||||
const [internalOpen, setInternalOpen] = useState<TabName>('stats');
|
||||
const open = tab ?? internalOpen; // controlled when `tab` is provided
|
||||
@@ -294,6 +295,7 @@ export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, qth,
|
||||
currentBand={slotCall ? (slotBand ?? '') : band}
|
||||
currentMode={slotCall ? (slotMode ?? '') : mode}
|
||||
bands={bands}
|
||||
modes={modes}
|
||||
hasCall={slotCall ? true : callsign.trim() !== ''}
|
||||
forCall={slotCall}
|
||||
onEditQso={onEditQso}
|
||||
|
||||
@@ -53,7 +53,13 @@ const ZERO: IcomState = {
|
||||
type Band = { l: string; hz: number };
|
||||
|
||||
const HF_BANDS: Band[] = [
|
||||
{ l: '160', hz: 1_840_000 }, { l: '80', hz: 3_750_000 }, { l: '40', hz: 7_100_000 },
|
||||
{ l: '160', hz: 1_840_000 }, { l: '80', hz: 3_750_000 },
|
||||
// 60 m: the middle of the IARU Region 1 allocation (5351.5-5366.5 kHz), which
|
||||
// every 60 m-capable rig can display. Where the band is channelised (the US)
|
||||
// the operator moves to their channel from here — the button is a way onto
|
||||
// the band, not a claim about what may be transmitted on it.
|
||||
{ l: '60', hz: 5_354_000 },
|
||||
{ l: '40', hz: 7_100_000 },
|
||||
{ l: '30', hz: 10_130_000 }, { l: '20', hz: 14_150_000 }, { l: '17', hz: 18_130_000 },
|
||||
{ l: '15', hz: 21_250_000 }, { l: '12', hz: 24_950_000 }, { l: '10', hz: 28_400_000 },
|
||||
];
|
||||
@@ -78,9 +84,36 @@ function bandsFor(model?: string): Band[] {
|
||||
return [...HF_BANDS, B6];
|
||||
}
|
||||
|
||||
// Mode buttons for the console (like RS-BA1's row). SetCATMode picks USB/LSB for
|
||||
// SSB by frequency and the rig's data variant for digital modes.
|
||||
const MODES = ['SSB', 'CW', 'RTTY', 'PSK', 'AM', 'FM', 'DATA'];
|
||||
// Mode buttons for the console (like RS-BA1's row).
|
||||
//
|
||||
// LSB and USB by NAME, not one "SSB" button that resolves by band: the band
|
||||
// convention is right for a logged mode and useless when the operator means
|
||||
// "put this radio in USB on 40 m", which the console could not express at all.
|
||||
//
|
||||
// PSK is native only on the 7610/7760/7851 class; every other rig NAKs 0x12, so
|
||||
// there the button is dead furniture — see modesFor. Soundcard PSK31 rides on
|
||||
// DATA, which every rig can do.
|
||||
const MODES_BASE = ['LSB', 'USB', 'CW', 'RTTY', 'AM', 'FM', 'DATA'];
|
||||
|
||||
function hasNativePSK(model?: string): boolean {
|
||||
const m = (model ?? '').toUpperCase();
|
||||
return m.includes('7610') || m.includes('7760') || m.includes('7851') ||
|
||||
m.includes('7800') || m.includes('7700');
|
||||
}
|
||||
|
||||
function modesFor(model?: string): string[] {
|
||||
if (!hasNativePSK(model)) return MODES_BASE;
|
||||
return [...MODES_BASE.slice(0, 4), 'PSK', ...MODES_BASE.slice(4)];
|
||||
}
|
||||
|
||||
// Which radios actually have an antenna selector on the CI-V command (0x12).
|
||||
// An IC-7300 has ONE socket: offering it ANT1/ANT2 was two buttons that could
|
||||
// only ever disagree with the front panel.
|
||||
function hasAntennaSelector(model?: string): boolean {
|
||||
const m = (model ?? '').toUpperCase();
|
||||
return m.includes('7610') || m.includes('7760') || m.includes('7851') ||
|
||||
m.includes('7800') || m.includes('7700') || m.includes('9700');
|
||||
}
|
||||
|
||||
// Attenuator steps are MODEL-dependent even though the CI-V command (0x11) is the
|
||||
// same: the value byte is the dB. The IC-7610 (and 7700/7800/7851) have a 6/12/18
|
||||
@@ -155,9 +188,21 @@ function icomWatts(pct: number): { w: number; defl: number } {
|
||||
return { w: Math.round(w), defl };
|
||||
}
|
||||
|
||||
function modeMatches(btn: string, cur?: string): boolean {
|
||||
// Which sideband a bare "SSB" means at this frequency — the same convention the
|
||||
// backend applies when it resolves the mode for the radio.
|
||||
function sideForHz(hz?: number): string | null {
|
||||
if (!hz || hz <= 0) return null;
|
||||
return hz < 10_000_000 ? 'LSB' : 'USB';
|
||||
}
|
||||
|
||||
function modeMatches(btn: string, cur?: string, hz?: number): boolean {
|
||||
if (!cur) return false;
|
||||
if (btn === 'SSB') return cur === 'SSB' || cur === 'USB' || cur === 'LSB';
|
||||
// A rig that reports the folded ADIF "SSB" still lights the side its
|
||||
// frequency implies, so the row is never blank on a phone contact.
|
||||
if (btn === 'USB' || btn === 'LSB') {
|
||||
if (cur === btn) return true;
|
||||
return cur === 'SSB' && btn === (sideForHz(hz) ?? '');
|
||||
}
|
||||
// The backend surfaces USB-D as the operator's digital default (FT8…), or as
|
||||
// plain DATA — either way it is the DATA button that should light.
|
||||
if (btn === 'DATA') return ['DATA', 'FT8', 'FT4', 'JS8', 'JT65', 'JT9', 'MFSK', 'OLIVIA'].includes(cur);
|
||||
@@ -507,9 +552,10 @@ export function IcomPanel({ onReportRST, isNetwork = false }: { onReportRST?: (r
|
||||
</div>
|
||||
</div>
|
||||
{/* Mode selector row (RS-BA1's SSB/CW/RTTY/PSK/AM/FM). */}
|
||||
<div className="grid grid-cols-7 border-t border-border/60 divide-x divide-border/60">
|
||||
{MODES.map((m) => {
|
||||
const on = modeMatches(m, curMode);
|
||||
<div className="grid border-t border-border/60 divide-x divide-border/60"
|
||||
style={{ gridTemplateColumns: `repeat(${modesFor(st.model).length}, minmax(0, 1fr))` }}>
|
||||
{modesFor(st.model).map((m) => {
|
||||
const on = modeMatches(m, curMode, mainHz);
|
||||
return (
|
||||
<button key={m} type="button" onClick={() => setMode(m)}
|
||||
className={cn('py-1.5 text-[11px] font-bold tracking-wide transition-colors',
|
||||
@@ -561,10 +607,12 @@ export function IcomPanel({ onReportRST, isNetwork = false }: { onReportRST?: (r
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Row label={t('icmp.antenna')}>
|
||||
<Segmented value={String(st.antenna)} options={[{ v: '1', l: 'ANT1' }, { v: '2', l: 'ANT2' }]}
|
||||
onChange={(v) => set({ antenna: parseInt(v) }, () => IcomSetAntenna(parseInt(v)))} />
|
||||
</Row>
|
||||
{hasAntennaSelector(st.model) && (
|
||||
<Row label={t('icmp.antenna')}>
|
||||
<Segmented value={String(st.antenna)} options={[{ v: '1', l: 'ANT1' }, { v: '2', l: 'ANT2' }]}
|
||||
onChange={(v) => set({ antenna: parseInt(v) }, () => IcomSetAntenna(parseInt(v)))} />
|
||||
</Row>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Clarifiers: RIT & ΔTX (XIT) — wheel or ± to shift, Ctrl+←/→ shifts RIT. */}
|
||||
@@ -588,7 +636,10 @@ export function IcomPanel({ onReportRST, isNetwork = false }: { onReportRST?: (r
|
||||
{(st.model ?? '').includes('7760') ? `${st.rf_power * 2} W` : st.rf_power}
|
||||
</span>
|
||||
</Row>
|
||||
{isPhone && (
|
||||
{/* Not phone-only: on USB-D the same control still sets what the radio
|
||||
transmits at, and hiding it left an operator who lives in FT8 with
|
||||
no mic gain at all. */}
|
||||
{(
|
||||
<Row label={t('icmp.mic')}>
|
||||
<Slider value={st.mic_gain} accent="#ef4444" onChange={(v) => set({ mic_gain: v }, () => IcomSetMicGain(v))} />
|
||||
<span className="w-8 text-right text-xs font-mono tabular-nums text-muted-foreground">{st.mic_gain}</span>
|
||||
|
||||
@@ -183,7 +183,7 @@ const en: Dict = {
|
||||
'dec.emptyFiltered': 'No decode matches these filters.',
|
||||
'sec.email': 'E-mail (SMTP)', 'sec.lookup': 'Callsign Lookup',
|
||||
'sec.ftx': 'FTx decodes', 'ftx.hint': 'What OpsLog does on its own with the digital decode stream.', 'ftx.enable': 'Auto-call', 'ftx.enableHint': 'answer a decode without clicking it', 'ftx.callWhen': 'Call a station that is:', 'ftx.watch': 'Watch list', 'ftx.watchHint': 'One callsign per line, wildcards allowed (4S7*, */P). A watched station is answered ahead of the criteria above.', 'ftx.watchOnlyIf': 'but only if it is also:', 'ftx.cooldown': 'Ignore a callsign for', 'ftx.warn': 'This keys your transmitter without asking. It answers CQ only, never while you are already transmitting, one station at a time, and every call is written to the log file with its reason. Halt stops it.', 'ftx.c_dxcc': 'a new DXCC entity', 'ftx.c_bandmode': 'a new band AND a new mode for the entity', 'ftx.c_band': 'a new band for the entity', 'ftx.c_mode': 'a new mode for the entity', 'ftx.c_slot': 'a new slot (band+mode never worked together)', 'ftx.c_grid': 'a new grid square', 'ftx.c_county': 'a new US county', 'ftx.c_pota': 'a new POTA park', 'ftx.c_pfx': 'a new WPX prefix',
|
||||
'sec.bands': 'Bands', 'sec.satellites': 'Satellites', 'sat.hint': 'The satellites this station works. They are offered as a dropdown on the satellite fields, in alphabetical order.', 'sat.listLabel': 'One satellite per line', 'sat.listHint': 'Written into SAT_NAME exactly as spelled here, so use the name LoTW and the awards expect — AO-91, not AO91. Leaving the list empty simply keeps the field a plain text box.', 'sec.modes': 'Modes & default RST', 'sec.dxhunter': 'DXHunter', 'dxh.intro': 'What you hunt — it decides the badges in the DX Cluster, the FT decodes and Chase new alike.', 'sec.cluster': 'DX Cluster',
|
||||
'sec.bands': 'Bands', 'sec.satellites': 'Satellites', 'sat.hint': 'The satellites this station works. They are offered as a dropdown on the satellite fields, in alphabetical order.', 'sat.listLabel': 'One satellite per line', 'sat.listHint': 'Written into SAT_NAME exactly as spelled here, so use the name LoTW and the awards expect — AO-91, not AO91. Leaving the list empty simply keeps the field a plain text box.', 'sec.modes': 'Modes & default RST', 'bsg.digCycle': 'Click to cycle through your digital modes', 'sec.dxhunter': 'DXHunter', 'dxh.intro': 'What you hunt — it decides the badges in the DX Cluster, the FT decodes and Chase new alike.', 'sec.cluster': 'DX Cluster',
|
||||
'sec.udp': 'Connections', 'sec.database': 'Database', 'sec.autostart': 'Autostart', 'sec.backup': 'Database backup', 'sec.uscounties': 'US Counties', 'nav.maintenance': 'Maintenance', 'sec.databases': 'Databases', 'db.hint': 'The reference data OpsLog keeps on disk. One line each, with what it holds and when it was last refreshed.', 'db.update': 'Update', 'db.never': 'never downloaded', 'db.cty': 'Country file (cty.dat)', 'db.ctyDetail': '{n} entities · file dated {d}', 'db.clublog': 'Club Log country exceptions', 'db.clublogDetail': '{n} exceptions · {d}', 'db.lotwUsers': 'LoTW users', 'db.lotwDetail': '{n} callsigns · {d}', 'db.scp': 'Super Check Partial (MASTER.SCP)', 'db.scpDetail': '{n} callsigns · {d}', 'db.uls': 'US counties (FCC ULS)', 'db.ulsDetail': '{n} callsigns · {d}', 'db.rda': 'Russian districts (RDA)', 'db.rdaDetail': '{n} callsigns, with their dated activity periods', 'db.rdaNote': 'built in', 'db.refLists': 'Award reference lists', 'db.refListsHint': 'Only the awards with an online source appear here; the others are shipped or edited by hand.', 'db.refDetail': '{n} references · {d}', 'db.refUpdated': '{code}: {n} references.', 'db.noRefLists': 'No award has an online reference list.', 'sec.rda': 'Russian districts (RDA)', 'rda.hint': 'The offline district database, and the one bulk operation it feeds.', 'rda.dbTitle': 'District database', 'rda.dbCount': '{n} Russian callsigns, each with the district it operates from and, where it moved, the dated periods it operated from each one. Built into OpsLog — nothing to download.', 'rda.backfillTitle': 'Fill the district on existing QSOs', 'rda.backfillIntro': 'Goes through every contact with a Russian entity and assigns its RDA reference, using the district the station was in ON THE DAY of the contact.', 'rda.useCurrent': 'Also use the current district for stations with no recorded history', 'rda.useCurrentHint': '(true for the great majority — the database records a history precisely for the callsigns that moved — but it is an assumption, not a dated fact)', 'rda.backfillRun': 'Fill districts', 'rda.backfillDone': '{s} Russian QSOs — {d} from a dated record, {c} from the current district, {u} unknown, {k} already had one.', 'rda.cmpTitle': 'Compare the two district sources', 'rda.cmpRunning': 'Comparing…', 'rda.cmpNoConflict': 'No disagreement — both sources say the same district everywhere.', 'rda.cmpNoRussian': 'No Russian contacts to compare.', 'rda.backfillRunning': 'Filling…', 'rda.cmpRun': 'Compare', 'rda.cmpDone': '{s} Russian QSOs — {a} identical, {d} differ, {l} only in the log, {b} only in the database', 'rda.cmpCall': 'Callsign', 'rda.cmpDate': 'Date', 'rda.cmpFromLog': 'Log (CNTY)', 'rda.cmpFromDb': 'RDA database', 'rda.cmpListHint': 'Showing {n} of {d} — click a callsign to open the contact.', 'rda.cmpOpen': 'Open this QSO', 'rda.cmpKind': 'Database', 'rda.cmpDated': 'dated', 'rda.cmpCurrent': 'current', 'rda.neverOverwrites': 'A reference you assigned by hand is never overwritten.', 'rda.cmpKeep': "Keep", 'rda.cmpKeepLog': "Keep the log's district for this contact", 'rda.cmpKeepDb': "Keep the database's district for this contact", 'rda.cmpApply': "Apply {n} decisions", 'rda.cmpAllDb': "keep the database everywhere", 'rda.cmpAllLog': "keep the log everywhere", 'rda.cmpClear': "clear the decisions", 'rda.cmpApplyHint': 'The chosen district is written into the contact — into CNTY and as its award reference — so the disagreement is settled and the contact counts for that district. Settled rows leave the list.',
|
||||
'sec.webpublish': 'Web publishing', 'wpub.hint': 'Publishes your log as a file for a website: a standalone HTML page or a CSV, written locally and optionally uploaded by FTP. It is refreshed when you log a QSO and, if you set an interval, on a timer.', 'wpub.enable': 'Publish the log to a file', 'wpub.fileSection': 'The file', 'wpub.format': 'Format', 'wpub.formatHtml': 'HTML page', 'wpub.formatCsv': 'CSV', 'wpub.folder': 'Output folder', 'wpub.browse': 'Browse…', 'wpub.fileName': 'File name', 'wpub.title': 'Page title', 'wpub.titlePh': 'blank = your callsign', 'wpub.count': 'Last N QSOs', 'wpub.every': 'Refresh every', 'wpub.everyHint': 'minutes — 0 = only when a QSO is logged', 'wpub.columns': 'Columns', 'wpub.columnsCount': '{n} of {total} chosen', 'wpub.columnsPick': 'Choose columns…', 'wpub.columnsSearch': 'Search a field…', 'wpub.removeColumn': 'Click to remove', 'wpub.columnsHint': 'Click to add or remove. The order shown here is the order in the file.', 'wpub.ftpEnable': 'Upload by FTP', 'wpub.ftpHost': 'Server / port', 'wpub.ftpUser': 'User', 'wpub.ftpPassword': 'Password', 'wpub.ftpFolder': 'Remote folder', 'wpub.ftpFileName': 'Remote file name', 'wpub.ftpTls': 'Use TLS (FTPS)', 'wpub.publishNow': 'Publish now', 'wpub.testFtp': 'Test connection', 'wpub.lastRun': 'Last run:', 'sec.adifmon': 'ADIF monitor',
|
||||
'sec.foldersync': 'Sync across PCs', 'sync.hint': 'Point every OpsLog at the SAME folder — one your PCs already synchronise (Seafile, OneDrive, Dropbox, a NAS share). Each machine writes what it logs there and reads the others; the databases themselves are never shared.', 'sync.enable': 'Keep my contacts in step across my PCs', 'sync.machine': 'This PC', 'sync.folder': 'Folder', 'sync.choose': 'Choose…', 'sync.state': 'State', 'sync.thisPc': 'This PC', 'sync.lastSync': 'Last check', 'sync.sent': 'Sent', 'sync.received': 'Received', 'sync.never': 'never', 'sync.noPeers': 'No other PC has written to this folder yet.', 'sync.behind': 'new contacts waiting', 'sync.now': 'Synchronise now', 'sync.applied': '{n} change(s) taken from the folder.', 'sync.saved': 'Saved.',
|
||||
@@ -703,7 +703,7 @@ const fr: Dict = {
|
||||
'dec.emptyFiltered': 'Aucun decode ne correspond a ces filtres.',
|
||||
'sec.email': 'E-mail (SMTP)', 'sec.lookup': "Recherche d'indicatif",
|
||||
'sec.ftx': 'Décodages FTx', 'ftx.hint': 'Ce qu’OpsLog fait de lui-même avec le flux de décodages numériques.', 'ftx.enable': 'Appel automatique', 'ftx.enableHint': 'répondre à un décodage sans cliquer', 'ftx.callWhen': 'Appeler une station qui est :', 'ftx.watch': 'Liste de surveillance', 'ftx.watchHint': 'Un indicatif par ligne, jokers acceptés (4S7*, */P). Une station surveillée est appelée avant les critères ci-dessus.', 'ftx.watchOnlyIf': 'mais seulement si elle est aussi :', 'ftx.cooldown': 'Ignorer un indicatif pendant', 'ftx.warn': 'Ceci met ton émetteur en marche sans te demander. Uniquement sur un CQ, jamais pendant que tu émets déjà, une station à la fois, et chaque appel est écrit dans le journal avec sa raison. Stop l’interrompt.', 'ftx.c_dxcc': 'une nouvelle entité DXCC', 'ftx.c_bandmode': 'une nouvelle bande ET un nouveau mode pour l’entité', 'ftx.c_band': 'une nouvelle bande pour l’entité', 'ftx.c_mode': 'un nouveau mode pour l’entité', 'ftx.c_slot': 'un nouveau slot (bande+mode jamais faits ensemble)', 'ftx.c_grid': 'un nouveau carré locator', 'ftx.c_county': 'un nouveau comté US', 'ftx.c_pota': 'un nouveau parc POTA', 'ftx.c_pfx': 'un nouveau préfixe WPX',
|
||||
'sec.bands': 'Bandes', 'sec.satellites': 'Satellites', 'sat.hint': "Les satellites que cette station travaille. Ils sont proposés en liste déroulante sur les champs satellite, par ordre alphabétique.", 'sat.listLabel': 'Un satellite par ligne', 'sat.listHint': "Inscrit dans SAT_NAME exactement tel qu'écrit ici : utilise le nom attendu par LoTW et les diplômes — AO-91, pas AO91. Une liste vide laisse simplement le champ en saisie libre.", 'sec.modes': 'Modes & RST par défaut', 'sec.dxhunter': 'DXHunter', 'dxh.intro': 'Ce que vous chassez — cela commande les badges du DX Cluster, des FT decodes et de Chase new.', 'sec.cluster': 'DX Cluster',
|
||||
'sec.bands': 'Bandes', 'sec.satellites': 'Satellites', 'sat.hint': "Les satellites que cette station travaille. Ils sont proposés en liste déroulante sur les champs satellite, par ordre alphabétique.", 'sat.listLabel': 'Un satellite par ligne', 'sat.listHint': "Inscrit dans SAT_NAME exactement tel qu'écrit ici : utilise le nom attendu par LoTW et les diplômes — AO-91, pas AO91. Une liste vide laisse simplement le champ en saisie libre.", 'sec.modes': 'Modes & RST par défaut', 'bsg.digCycle': 'Cliquer pour faire défiler vos modes numériques', 'sec.dxhunter': 'DXHunter', 'dxh.intro': 'Ce que vous chassez — cela commande les badges du DX Cluster, des FT decodes et de Chase new.', 'sec.cluster': 'DX Cluster',
|
||||
'sec.udp': 'Connexions', 'sec.database': 'Base de données', 'sec.autostart': 'Démarrage auto', 'sec.backup': 'Sauvegarde base', 'sec.uscounties': 'Comtés US', 'nav.maintenance': 'Maintenance', 'sec.databases': 'Bases de données', 'db.hint': 'Les données de référence qu’OpsLog garde sur disque. Une ligne chacune, avec ce qu’elle contient et sa dernière actualisation.', 'db.update': 'Mettre à jour', 'db.never': 'jamais téléchargée', 'db.cty': 'Fichier pays (cty.dat)', 'db.ctyDetail': '{n} entités · fichier daté du {d}', 'db.clublog': 'Exceptions pays Club Log', 'db.clublogDetail': '{n} exceptions · {d}', 'db.lotwUsers': 'Utilisateurs LoTW', 'db.lotwDetail': '{n} indicatifs · {d}', 'db.scp': 'Super Check Partial (MASTER.SCP)', 'db.scpDetail': '{n} indicatifs · {d}', 'db.uls': 'Comtés US (FCC ULS)', 'db.ulsDetail': '{n} indicatifs · {d}', 'db.rda': 'Districts russes (RDA)', 'db.rdaDetail': '{n} indicatifs, avec leurs périodes d’activité datées', 'db.rdaNote': 'intégrée', 'db.refLists': 'Listes de références des diplômes', 'db.refListsHint': 'Seuls les diplômes ayant une source en ligne apparaissent ici ; les autres sont livrés ou édités à la main.', 'db.refDetail': '{n} références · {d}', 'db.refUpdated': '{code} : {n} références.', 'db.noRefLists': 'Aucun diplôme n’a de liste de références en ligne.', 'sec.rda': 'Districts russes (RDA)', 'rda.hint': 'La base de districts hors ligne, et l’unique opération de masse qu’elle alimente.', 'rda.dbTitle': 'Base des districts', 'rda.dbCount': '{n} indicatifs russes, chacun avec le district d’où il émet et, pour ceux qui ont déménagé, les périodes datées passées dans chacun. Intégrée à OpsLog — rien à télécharger.', 'rda.backfillTitle': 'Renseigner le district sur les QSO existants', 'rda.backfillIntro': 'Parcourt tous les contacts avec une entité russe et attribue leur référence RDA, en utilisant le district où se trouvait la station LE JOUR du contact.', 'rda.useCurrent': 'Utiliser aussi le district actuel pour les stations sans historique connu', 'rda.useCurrentHint': '(vrai pour la grande majorité — la base enregistre un historique justement pour les indicatifs qui ont bougé — mais c’est une supposition, pas un fait daté)', 'rda.backfillRun': 'Renseigner les districts', 'rda.backfillDone': '{s} QSO russes — {d} depuis une période datée, {c} depuis le district actuel, {u} inconnus, {k} en avaient déjà un.', 'rda.cmpTitle': 'Comparer les deux sources de district', 'rda.cmpRunning': 'Comparaison…', 'rda.cmpNoConflict': 'Aucune divergence — les deux sources donnent partout le même district.', 'rda.cmpNoRussian': 'Aucun contact russe à comparer.', 'rda.backfillRunning': 'Remplissage…', 'rda.cmpRun': 'Comparer', 'rda.cmpDone': '{s} QSO russes — {a} identiques, {d} divergents, {l} seulement dans le log, {b} seulement dans la base', 'rda.cmpCall': 'Indicatif', 'rda.cmpDate': 'Date', 'rda.cmpFromLog': 'Log (CNTY)', 'rda.cmpFromDb': 'Base RDA', 'rda.cmpListHint': '{n} affichés sur {d} — cliquer un indicatif ouvre le contact.', 'rda.cmpOpen': 'Ouvrir ce QSO', 'rda.cmpKind': 'Base', 'rda.cmpDated': 'daté', 'rda.cmpCurrent': 'courant', 'rda.neverOverwrites': 'Une référence attribuée à la main n’est jamais écrasée.', 'rda.cmpKeep': "Garder", 'rda.cmpKeepLog': "Garder le district du log pour ce contact", 'rda.cmpKeepDb': "Garder le district de la base pour ce contact", 'rda.cmpApply': "Appliquer {n} décisions", 'rda.cmpAllDb': "garder la base partout", 'rda.cmpAllLog': "garder le log partout", 'rda.cmpClear': "effacer les décisions", 'rda.cmpApplyHint': "Le district choisi est écrit dans le contact — dans CNTY et comme référence de diplôme — donc la divergence est réglée et le contact compte pour ce district. Les lignes réglées quittent la liste.",
|
||||
'sec.webpublish': 'Publication web', 'wpub.hint': "Publie ton journal dans un fichier destiné à un site web : une page HTML autonome ou un CSV, écrit en local et envoyé par FTP si tu le souhaites. Il est rafraîchi à chaque QSO enregistré et, si tu règles un intervalle, périodiquement.", 'wpub.enable': 'Publier le journal dans un fichier', 'wpub.fileSection': 'Le fichier', 'wpub.format': 'Format', 'wpub.formatHtml': 'Page HTML', 'wpub.formatCsv': 'CSV', 'wpub.folder': 'Dossier de sortie', 'wpub.browse': 'Parcourir…', 'wpub.fileName': 'Nom du fichier', 'wpub.title': 'Titre de la page', 'wpub.titlePh': 'vide = ton indicatif', 'wpub.count': 'N derniers QSO', 'wpub.every': 'Rafraîchir toutes les', 'wpub.everyHint': 'minutes — 0 = seulement à chaque QSO', 'wpub.columns': 'Colonnes', 'wpub.columnsCount': '{n} sur {total} choisis', 'wpub.columnsPick': 'Choisir les colonnes…', 'wpub.columnsSearch': 'Chercher un champ…', 'wpub.removeColumn': 'Cliquer pour retirer', 'wpub.columnsHint': 'Clique pour ajouter ou retirer. L ordre affiché ici est celui du fichier.', 'wpub.ftpEnable': 'Envoyer par FTP', 'wpub.ftpHost': 'Serveur / port', 'wpub.ftpUser': 'Utilisateur', 'wpub.ftpPassword': 'Mot de passe', 'wpub.ftpFolder': 'Dossier distant', 'wpub.ftpFileName': 'Nom du fichier distant', 'wpub.ftpTls': 'Utiliser TLS (FTPS)', 'wpub.publishNow': 'Publier maintenant', 'wpub.testFtp': 'Tester la connexion', 'wpub.lastRun': 'Dernière exécution :', 'sec.adifmon': 'Moniteur ADIF',
|
||||
'sec.foldersync': 'Synchro entre PC', 'sync.hint': 'Fais pointer chaque OpsLog vers le MÊME dossier — un dossier que tes PC synchronisent déjà (Seafile, OneDrive, Dropbox, un partage NAS). Chaque machine y écrit ce qu’elle enregistre et lit celui des autres ; les bases de données, elles, ne sont jamais partagées.', 'sync.enable': 'Garder mes contacts à jour sur tous mes PC', 'sync.machine': 'Ce PC', 'sync.folder': 'Dossier', 'sync.choose': 'Choisir…', 'sync.state': 'État', 'sync.thisPc': 'Ce PC', 'sync.lastSync': 'Dernière vérification', 'sync.sent': 'Envoyés', 'sync.received': 'Reçus', 'sync.never': 'jamais', 'sync.noPeers': 'Aucun autre PC n’a encore écrit dans ce dossier.', 'sync.behind': 'nouveaux contacts en attente', 'sync.now': 'Synchroniser maintenant', 'sync.applied': '{n} changement(s) repris du dossier.', 'sync.saved': 'Enregistré.',
|
||||
|
||||
@@ -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.27.7';
|
||||
export const APP_VERSION = '0.27.8';
|
||||
|
||||
// Author / credits, shown in Help -> About.
|
||||
export const APP_AUTHOR = 'F4BPO';
|
||||
|
||||
@@ -1543,6 +1543,14 @@ func (b *IcomSerial) modeCode(mode string) (code byte, data bool, err error) {
|
||||
return civ.ModeCW, false, nil
|
||||
case "SSB":
|
||||
return usb, false, nil
|
||||
case "USB":
|
||||
// The SIDEBAND, asked for by name. "SSB" resolves to whichever side the
|
||||
// band convention wants, which is right for a logged mode and useless
|
||||
// when the operator means "put this radio in USB" — on 40 m there was no
|
||||
// way to say it at all, and the console's own button could not either.
|
||||
return civ.ModeUSB, false, nil
|
||||
case "LSB":
|
||||
return civ.ModeLSB, false, nil
|
||||
case "AM":
|
||||
return civ.ModeAM, false, nil
|
||||
case "FM":
|
||||
|
||||
+24
-9
@@ -2013,7 +2013,7 @@ type WorkedBefore struct {
|
||||
// at all about yesterday.
|
||||
type BandStatus struct {
|
||||
Band string `json:"band"` // ADIF lowercase band, e.g. "20m"
|
||||
Class string `json:"class"` // "PH" | "CW" | "DIG"
|
||||
Class string `json:"class"` // "PH" | "CW" | "DIG", or a raw digital mode ("FT8", "RTTY"…)
|
||||
Status string `json:"status"` // "call_c" | "call_w" | "dxcc_c" | "dxcc_w"
|
||||
// Call is "", "w" (worked with this callsign) or "c" (confirmed with it).
|
||||
Call string `json:"call,omitempty"`
|
||||
@@ -2402,17 +2402,32 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int,
|
||||
return wb, fmt.Errorf("scan band status: %w", err)
|
||||
}
|
||||
code := bandStatusCode(callW == 1, callC == 1, dxccConfirmed == 1)
|
||||
k := cellKey{band: band, class: modeClass(mode)}
|
||||
if cur, ok := best[k]; !ok || code > cur {
|
||||
best[k] = code
|
||||
keys := []cellKey{{band: band, class: modeClass(mode)}}
|
||||
// The DIGITAL row can be cycled through the individual modes in the UI —
|
||||
// FT8, then FT4, then RTTY — so the same cell is also published under the
|
||||
// raw mode name. The query already grouped by mode; only the collapse to
|
||||
// a class threw that away, and re-asking the database for it would be a
|
||||
// second scan to learn what we had just read.
|
||||
//
|
||||
// Digital only: PH and CW have nothing to cycle through, and publishing
|
||||
// "SSB" beside "PH" would just double the payload.
|
||||
if um := strings.ToUpper(mode); modeClass(mode) == "DIG" && um != "" {
|
||||
keys = append(keys, cellKey{band: band, class: um})
|
||||
}
|
||||
for _, k := range keys {
|
||||
if cur, ok := best[k]; !ok || code > cur {
|
||||
best[k] = code
|
||||
}
|
||||
}
|
||||
// Confirmed beats worked here too, and neither is ever erased by the
|
||||
// entity: this is only ever about the callsign.
|
||||
switch {
|
||||
case callC == 1:
|
||||
callByCell[k] = "c"
|
||||
case callW == 1 && callByCell[k] == "":
|
||||
callByCell[k] = "w"
|
||||
for _, k := range keys {
|
||||
switch {
|
||||
case callC == 1:
|
||||
callByCell[k] = "c"
|
||||
case callW == 1 && callByCell[k] == "":
|
||||
callByCell[k] = "w"
|
||||
}
|
||||
}
|
||||
}
|
||||
statusRows.Close()
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
const (
|
||||
// appVersion is stamped on every heartbeat (and could feed the About box).
|
||||
appVersion = "0.27.7"
|
||||
appVersion = "0.27.8"
|
||||
|
||||
// posthogHost is the PostHog ingestion endpoint. EU cloud by default; change
|
||||
// to https://us.i.posthog.com for a US project.
|
||||
|
||||
Reference in New Issue
Block a user