feat(appearance): the band/mode matrix colours can be chosen
The PH/CW/DIG grid in the Stats panel is the fastest read in the app and its palette was fixed per theme. Settings -> Appearance now offers the six: the four status fills, the never-worked fill, and the ring on the cell being entered. Stored as OVERRIDES, not as a palette. Each of the twelve themes ships an --mx-* ramp tuned to its own background, so an operator who only wants a different green must not thereby freeze the other four to the theme they happened to be using that day. An empty value means "whatever the theme says"; the chosen ones are stamped inline on <html>, where they win over every theme; switching the feature off hands the colours straight back. The pickers are seeded from what the matrix is painting at that moment rather than from a fixed palette, so the choice starts from the colours in front of the operator. A new --mx-cur token carries the current-entry ring: it follows --warning by default, so it stays theme-correct on all twelve, but can be recoloured without dragging every other warning in the app along. The legend under the matrix and its cell tooltips were hardcoded English. They now go through t() with the same keys as the pickers, so the grid and the settings cannot disagree about which green is which.
This commit is contained in:
@@ -245,15 +245,16 @@ const (
|
||||
keyMotorBandFreqs = "motor.band_freqs" // per-band tune frequency: "40m=7100,20m=14150"
|
||||
keyChaseNewGrids = "cluster.chase_grids" // "1" → persist learnt locators across restarts
|
||||
keyRowColors = "appearance.row_colors"
|
||||
keyMotorType = "ultrabeam.type" // "ultrabeam" | "steppir" (default ultrabeam)
|
||||
keyMotorTransport = "ultrabeam.transport" // "tcp" | "serial" (default tcp)
|
||||
keyMotorCOM = "ultrabeam.com" // serial device name (COM3, /dev/ttyUSB0)
|
||||
keyMotorBaud = "ultrabeam.baud" // serial baud (SteppIR default 9600)
|
||||
keyMotorTXInhibit = "ultrabeam.tx_inhibit" // "1" → block Flex TX while the antenna is moving
|
||||
keyMotorFreqMin = "ultrabeam.freq_min" // SteppIR tunable range low edge (MHz); out-of-range = don't follow/inhibit
|
||||
keyMotorFreqMax = "ultrabeam.freq_max" // SteppIR tunable range high edge (MHz)
|
||||
keyMotorBands = "ultrabeam.bands" // CSV of bands the antenna covers (e.g. "40m,20m,17m,…"); the follow filter
|
||||
keyStationDevices = "station.devices" // JSON list of relay boards for the Station Control tab
|
||||
keyMatrixColors = "appearance.matrix_colors" // band/mode matrix palette overrides
|
||||
keyMotorType = "ultrabeam.type" // "ultrabeam" | "steppir" (default ultrabeam)
|
||||
keyMotorTransport = "ultrabeam.transport" // "tcp" | "serial" (default tcp)
|
||||
keyMotorCOM = "ultrabeam.com" // serial device name (COM3, /dev/ttyUSB0)
|
||||
keyMotorBaud = "ultrabeam.baud" // serial baud (SteppIR default 9600)
|
||||
keyMotorTXInhibit = "ultrabeam.tx_inhibit" // "1" → block Flex TX while the antenna is moving
|
||||
keyMotorFreqMin = "ultrabeam.freq_min" // SteppIR tunable range low edge (MHz); out-of-range = don't follow/inhibit
|
||||
keyMotorFreqMax = "ultrabeam.freq_max" // SteppIR tunable range high edge (MHz)
|
||||
keyMotorBands = "ultrabeam.bands" // CSV of bands the antenna covers (e.g. "40m,20m,17m,…"); the follow filter
|
||||
keyStationDevices = "station.devices" // JSON list of relay boards for the Station Control tab
|
||||
|
||||
// Antenna Genius (4O3A) antenna switch — Hardware → Antenna Genius. TCP
|
||||
// port is fixed at 9007, so only the IP is configurable.
|
||||
|
||||
@@ -138,6 +138,70 @@ func normRowColors(s RowColorSettings) RowColorSettings {
|
||||
return out
|
||||
}
|
||||
|
||||
// MatrixColors recolours the band/mode matrix — the PH/CW/DIG grid in the Stats
|
||||
// panel, whose five fills and current-entry ring are the fastest read in the
|
||||
// whole app and the one an operator is most likely to want in their own colours.
|
||||
//
|
||||
// Every colour is OPTIONAL and an empty one keeps whatever the active theme
|
||||
// paints. That is why this stores OVERRIDES rather than a palette: each of the
|
||||
// twelve themes ships a matrix ramp tuned to its own background, and an operator
|
||||
// who only wants a different green must not thereby freeze the other four to the
|
||||
// theme they happened to be using the day they picked it.
|
||||
type MatrixColors struct {
|
||||
// Enabled off leaves the theme's own ramp untouched, so switching it off is a
|
||||
// genuine revert and not "some other set of colours".
|
||||
Enabled bool `json:"enabled"`
|
||||
CallConfirmed string `json:"call_confirmed"`
|
||||
CallWorked string `json:"call_worked"`
|
||||
EntityConfirmed string `json:"entity_confirmed"`
|
||||
EntityWorked string `json:"entity_worked"`
|
||||
NotWorked string `json:"not_worked"`
|
||||
CurrentEntry string `json:"current_entry"`
|
||||
}
|
||||
|
||||
// normMatrixColors keeps only plain hex values. Anything else becomes "" — i.e.
|
||||
// "use the theme's" — because these are written straight into a CSS custom
|
||||
// property, and the same rule as hexColor's own comment applies: what cannot be
|
||||
// trusted into a stylesheet is refused rather than passed through.
|
||||
func normMatrixColors(c MatrixColors) MatrixColors {
|
||||
clean := func(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if hexColor.MatchString(s) {
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return MatrixColors{
|
||||
Enabled: c.Enabled,
|
||||
CallConfirmed: clean(c.CallConfirmed),
|
||||
CallWorked: clean(c.CallWorked),
|
||||
EntityConfirmed: clean(c.EntityConfirmed),
|
||||
EntityWorked: clean(c.EntityWorked),
|
||||
NotWorked: clean(c.NotWorked),
|
||||
CurrentEntry: clean(c.CurrentEntry),
|
||||
}
|
||||
}
|
||||
|
||||
// GetMatrixColors returns the operator's matrix palette overrides. All-empty
|
||||
// (the default) means "whatever the theme says".
|
||||
func (a *App) GetMatrixColors() MatrixColors {
|
||||
var c MatrixColors
|
||||
if raw := a.settingOr(keyMatrixColors, ""); raw != "" {
|
||||
_ = json.Unmarshal([]byte(raw), &c)
|
||||
}
|
||||
return normMatrixColors(c)
|
||||
}
|
||||
|
||||
// SaveMatrixColors persists them.
|
||||
func (a *App) SaveMatrixColors(c MatrixColors) error {
|
||||
b, err := json.Marshal(normMatrixColors(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.setSetting(keyMatrixColors, string(b))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRowColors returns the row-colouring configuration, defaults included so the
|
||||
// panel never has to invent one.
|
||||
func (a *App) GetRowColors() RowColorSettings {
|
||||
|
||||
@@ -94,7 +94,8 @@ import { ShutdownProgress } from '@/components/ShutdownProgress';
|
||||
import { ClusterGrid } from '@/components/ClusterGrid';
|
||||
import { cleanSpotter, inferSpotMode, spotModeCategory, spotStatusKey } from '@/lib/spot';
|
||||
import { applySpotDisplay, readSpotDisplayOptions, spotIsWorked, SPOT_DISPLAY_OPTIONS_EXPOSED } from '@/lib/spotDisplay';
|
||||
import { GetRowColors, GetSpotTTLMinutes, IsNewUSCounty } from '../wailsjs/go/main/App';
|
||||
import { GetMatrixColors, GetRowColors, GetSpotTTLMinutes, IsNewUSCounty } from '../wailsjs/go/main/App';
|
||||
import { applyMatrixColors } from '@/lib/matrixColors';
|
||||
import { WorkedBeforeGrid } from '@/components/WorkedBeforeGrid';
|
||||
import { NetControlPanel } from '@/components/NetControlPanel';
|
||||
import { ContestPanel, CONTEST_DEFAULT, type ContestSession } from '@/components/ContestPanel';
|
||||
@@ -2017,6 +2018,12 @@ export default function App() {
|
||||
// settings dialog closes, which is the only place it changes.
|
||||
const [rowColors, setRowColors] = useState<any>(null);
|
||||
useEffect(() => { GetRowColors().then(setRowColors).catch(() => {}); }, [showSettings]);
|
||||
// Band/mode matrix palette overrides (Settings → Appearance). Stamped onto
|
||||
// <html> rather than held in state: the matrix reads CSS custom properties, so
|
||||
// nothing re-renders and no component has to be told about the colours. Same
|
||||
// reload trigger as the row colours — the settings dialog is where they change,
|
||||
// and the panel already previews live while it is open.
|
||||
useEffect(() => { GetMatrixColors().then((c) => applyMatrixColors(c as any)).catch(() => {}); }, [showSettings]);
|
||||
// Spot lifetime (Settings → DX Cluster). Spots are actually REMOVED rather
|
||||
// than filtered at render: the cluster list, every band map and the counts all
|
||||
// read the same array, so pruning it once is what makes the setting mean the
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { GetRowColors, SaveRowColors } from '../../wailsjs/go/main/App';
|
||||
import { GetMatrixColors, GetRowColors, SaveMatrixColors, SaveRowColors } from '../../wailsjs/go/main/App';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { RowColorSettings } from '@/lib/rowColors';
|
||||
import {
|
||||
MATRIX_VARS, applyMatrixColors, effectiveMatrixColor, emptyMatrixColors,
|
||||
type MatrixColors,
|
||||
} from '@/lib/matrixColors';
|
||||
|
||||
// A fixed palette plus a free picker. Muted values on purpose: they are
|
||||
// composited at low opacity over a dark grid, where a saturated colour reads as
|
||||
@@ -30,6 +34,111 @@ const CHANNEL_LABELS: Record<string, string> = {
|
||||
qsl: 'appr.chQsl', lotw: 'LoTW', eqsl: 'eQSL', qrz: 'QRZ.com',
|
||||
};
|
||||
|
||||
// MatrixColorsSection recolours the band/mode matrix — the PH/CW/DIG grid in the
|
||||
// Stats panel.
|
||||
//
|
||||
// The pickers are seeded from what the matrix is painting RIGHT NOW (the active
|
||||
// theme's ramp, or an existing override), not from a fixed palette: the operator
|
||||
// starts from the colours in front of them and moves one, instead of being
|
||||
// handed six values that may belong to a theme they stopped using. Every change
|
||||
// is applied to the live document at once, so the sample row below is the real
|
||||
// thing rather than a mock-up of it.
|
||||
function MatrixColorsSection() {
|
||||
const { t } = useI18n();
|
||||
const [cfg, setCfg] = useState<MatrixColors | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setCfg((await GetMatrixColors()) as any);
|
||||
} catch {
|
||||
setCfg(emptyMatrixColors());
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const save = (next: MatrixColors) => {
|
||||
setCfg(next);
|
||||
applyMatrixColors(next); // live, before the round trip — the panel must not lag the choice
|
||||
SaveMatrixColors(next as any).catch(() => {});
|
||||
};
|
||||
|
||||
// Turning it ON with nothing stored would change nothing at all and read as a
|
||||
// broken switch, so the empty slots are filled from the theme's current ramp:
|
||||
// the operator sees six swatches that match the grid and edits from there.
|
||||
const enable = (on: boolean) => {
|
||||
if (!cfg) return;
|
||||
if (!on) {
|
||||
save({ ...cfg, enabled: false });
|
||||
return;
|
||||
}
|
||||
const seeded = { ...cfg, enabled: true };
|
||||
for (const { key, cssVar } of MATRIX_VARS) {
|
||||
if (!String(seeded[key] ?? '').trim()) seeded[key] = effectiveMatrixColor(cssVar);
|
||||
}
|
||||
save(seeded);
|
||||
};
|
||||
|
||||
// Reset clears the overrides but keeps the section switched on, then re-seeds
|
||||
// from the theme — "back to the theme's colours", which is what an operator
|
||||
// means by reset here, rather than "switch the whole feature off".
|
||||
const reset = () => {
|
||||
if (!cfg) return;
|
||||
applyMatrixColors({ ...emptyMatrixColors(), enabled: false });
|
||||
const seeded = { ...emptyMatrixColors(), enabled: true };
|
||||
for (const { key, cssVar } of MATRIX_VARS) seeded[key] = effectiveMatrixColor(cssVar);
|
||||
save(seeded);
|
||||
};
|
||||
|
||||
if (!cfg) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-3 border-t border-border/60 pt-4">
|
||||
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={cfg.enabled} className="mt-0.5" onCheckedChange={(c) => enable(!!c)} />
|
||||
<span>
|
||||
{t('appr.matrixEnable')}{' '}
|
||||
<span className="text-xs text-muted-foreground">{t('appr.matrixHint')}</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{cfg.enabled && (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
|
||||
{MATRIX_VARS.map(({ key, cssVar, label }) => (
|
||||
<label key={key} className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input
|
||||
type="color"
|
||||
value={String(cfg[key] || '').trim() || effectiveMatrixColor(cssVar)}
|
||||
onChange={(e) => save({ ...cfg, [key]: e.target.value })}
|
||||
className="size-6 rounded-md border border-border bg-transparent p-0 cursor-pointer shrink-0"
|
||||
/>
|
||||
{t(label)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* The matrix as it will actually look: same tokens, same shapes. */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs text-muted-foreground w-10 shrink-0">{t('appr.matrixSample')}</span>
|
||||
<span className="inline-block w-7 h-5 rounded bg-mx-call-conf" />
|
||||
<span className="inline-block w-7 h-5 rounded bg-mx-call-work" />
|
||||
<span className="inline-block w-7 h-5 rounded bg-mx-dx-conf" />
|
||||
<span className="inline-block w-7 h-5 rounded bg-mx-dx-work" />
|
||||
<span className="inline-block w-7 h-5 rounded bg-mx-none" />
|
||||
<span className="inline-block w-7 h-5 rounded bg-mx-none ring-2 ring-mx-cur ring-inset" />
|
||||
</div>
|
||||
|
||||
<button type="button" onClick={reset}
|
||||
className="text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground">
|
||||
{t('appr.matrixReset')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppearancePanel() {
|
||||
const { t } = useI18n();
|
||||
const [cfg, setCfg] = useState<RowColorSettings | null>(null);
|
||||
@@ -148,6 +257,8 @@ export function AppearancePanel() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MatrixColorsSection />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { sunTimes } from '@/lib/sun';
|
||||
import { isQSLConfirmed } from '@/lib/qsl';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { BandSlotQSOs } from '../../wailsjs/go/main/App';
|
||||
import type { WorkedBeforeView } from '@/types';
|
||||
|
||||
@@ -76,28 +77,31 @@ const STATUS_CLASSES: Record<string, string> = {
|
||||
dxcc_w: 'bg-mx-dx-work',
|
||||
};
|
||||
|
||||
// Legend entries, in the same colour order as the cells. swatch = the
|
||||
// background class (or a special ring marker for the current-entry cell).
|
||||
// Legend entries, in the same colour order as the cells — and the same order and
|
||||
// i18n keys the Appearance panel's colour pickers use, so the two can never
|
||||
// disagree about which green is which. swatch = the background class (or a
|
||||
// special ring marker for the current-entry cell).
|
||||
const LEGEND: { swatch: string; ring?: boolean; label: string }[] = [
|
||||
{ swatch: 'bg-mx-call-conf', label: 'Call confirmed' },
|
||||
{ swatch: 'bg-mx-call-work', label: 'Call worked' },
|
||||
{ swatch: 'bg-mx-dx-conf', label: 'Entity confirmed' },
|
||||
{ swatch: 'bg-mx-dx-work', label: 'Entity worked' },
|
||||
{ swatch: 'bg-mx-none', label: 'Not worked' },
|
||||
{ swatch: 'bg-mx-none', ring: true, label: 'Current entry' },
|
||||
{ swatch: 'bg-mx-call-conf', label: 'mx.callConf' },
|
||||
{ swatch: 'bg-mx-call-work', label: 'mx.callWork' },
|
||||
{ swatch: 'bg-mx-dx-conf', label: 'mx.dxConf' },
|
||||
{ swatch: 'bg-mx-dx-work', label: 'mx.dxWork' },
|
||||
{ swatch: 'bg-mx-none', label: 'mx.none' },
|
||||
{ swatch: 'bg-mx-none', ring: true, label: 'mx.current' },
|
||||
];
|
||||
|
||||
function cellTitle(band: string, cls: string, status: string, current: boolean): string {
|
||||
function cellTitle(t: (k: string) => string, band: string, cls: string, status: string, current: boolean): string {
|
||||
const desc =
|
||||
status === 'call_c' ? 'This callsign confirmed' :
|
||||
status === 'call_w' ? 'This callsign worked (not confirmed)' :
|
||||
status === 'dxcc_c' ? 'Entity confirmed (other callsign)' :
|
||||
status === 'dxcc_w' ? 'Entity worked (other callsign)' :
|
||||
'Never worked';
|
||||
return `${band} ${cls}: ${desc}${current ? ' — current entry' : ''}`;
|
||||
status === 'call_c' ? t('mx.tipCallConf') :
|
||||
status === 'call_w' ? t('mx.tipCallWork') :
|
||||
status === 'dxcc_c' ? t('mx.tipDxConf') :
|
||||
status === 'dxcc_w' ? t('mx.tipDxWork') :
|
||||
t('mx.tipNone');
|
||||
return `${band} ${cls}: ${desc}${current ? ' — ' + t('mx.current') : ''}`;
|
||||
}
|
||||
|
||||
export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, 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);
|
||||
// Columns from the operator's configured bands (so the matrix shows only the
|
||||
@@ -308,7 +312,7 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal
|
||||
return (
|
||||
<td
|
||||
key={b.tag}
|
||||
title={cellTitle(b.tag, cls, st, isCurrent) + (st ? ' — click to list the QSOs' : '')}
|
||||
title={cellTitle(t, b.tag, cls, st, isCurrent) + (st ? ' — ' + t('mx.tipClick') : '')}
|
||||
onClick={st ? () => setSlot({ band: b.tag, cls }) : undefined}
|
||||
className={cn(
|
||||
'w-[28px] h-[24px] rounded transition-colors p-0',
|
||||
@@ -316,7 +320,7 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal
|
||||
// Only a filled cell has anything to show — an empty one
|
||||
// stays inert rather than opening a "no QSOs" dialog.
|
||||
st && 'cursor-pointer hover:brightness-110',
|
||||
isCurrent && 'ring-2 ring-warning ring-inset',
|
||||
isCurrent && 'ring-2 ring-mx-cur ring-inset',
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@@ -335,10 +339,10 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal
|
||||
className={cn(
|
||||
'inline-block size-3 rounded shrink-0',
|
||||
l.swatch,
|
||||
l.ring && 'ring-2 ring-warning ring-inset',
|
||||
l.ring && 'ring-2 ring-mx-cur ring-inset',
|
||||
)}
|
||||
/>
|
||||
{l.label}
|
||||
{t(l.label)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -114,7 +114,17 @@ const en: Dict = {
|
||||
'nav.user': 'User Configuration', 'nav.software': 'Software Configuration', 'nav.hardware': 'Hardware Configuration', 'nav.lists': 'Lists',
|
||||
'sec.station': 'Station Information', 'sec.profiles': 'Profiles', 'sec.operating': 'Operating conditions',
|
||||
'sec.confirmations': 'Confirmations', 'sec.external': 'External services',
|
||||
'sec.general': 'General', 'sec.appearance': 'Appearance', 'appr.enable': 'Colour whole rows by QSL status', 'appr.enableHint': '(in the log grid, like Logger32)', 'appr.orderHint': 'A contact is often several of these at once — the first rule that matches decides the colour.', 'appr.ruleToSend': 'To be sent', 'appr.ruleConfirmed': 'Confirmed', 'appr.ruleSent': 'QSL sent', 'appr.ruleWorked': 'Worked, nothing sent', 'appr.chQsl': 'Paper QSL', 'appr.custom': 'Pick any colour', 'appr.style': 'Style', 'appr.styleBar': 'Left stripe', 'appr.styleTint': 'Filled row', 'appr.styleBoth': 'Both', 'appr.intensity': 'Strength', 'appr.bandmapLotw': 'Mark LoTW users on the band map', 'appr.bandmapLotwHint': '(the same L badge the cluster list uses)', 'sec.email': 'E-mail (SMTP)', 'sec.lookup': 'Callsign Lookup',
|
||||
'sec.general': 'General', 'sec.appearance': 'Appearance', 'appr.enable': 'Colour whole rows by QSL status', 'appr.enableHint': '(in the log grid, like Logger32)', 'appr.orderHint': 'A contact is often several of these at once — the first rule that matches decides the colour.', 'appr.ruleToSend': 'To be sent', 'appr.ruleConfirmed': 'Confirmed', 'appr.ruleSent': 'QSL sent', 'appr.ruleWorked': 'Worked, nothing sent', 'appr.chQsl': 'Paper QSL', 'appr.custom': 'Pick any colour', 'appr.style': 'Style', 'appr.styleBar': 'Left stripe', 'appr.styleTint': 'Filled row', 'appr.styleBoth': 'Both', 'appr.intensity': 'Strength', 'appr.bandmapLotw': 'Mark LoTW users on the band map', 'appr.bandmapLotwHint': '(the same L badge the cluster list uses)',
|
||||
'appr.matrixEnable': 'Choose the band/mode matrix colours', 'appr.matrixHint': '(the PH/CW/DIG grid in Stats — off, each theme uses its own)',
|
||||
'appr.matrixSample': 'Sample', 'appr.matrixReset': 'Back to the theme’s colours',
|
||||
// Matrix legend + colour names. One set of labels for the grid's legend, its
|
||||
// cell tooltips and the colour pickers, so they can never drift apart.
|
||||
'mx.callConf': 'Call confirmed', 'mx.callWork': 'Call worked', 'mx.dxConf': 'Entity confirmed',
|
||||
'mx.dxWork': 'Entity worked', 'mx.none': 'Not worked', 'mx.current': 'Current entry',
|
||||
'mx.tipCallConf': 'This callsign confirmed', 'mx.tipCallWork': 'This callsign worked (not confirmed)',
|
||||
'mx.tipDxConf': 'Entity confirmed (other callsign)', 'mx.tipDxWork': 'Entity worked (other callsign)',
|
||||
'mx.tipNone': 'Never worked', 'mx.tipClick': 'click to list the QSOs',
|
||||
'sec.email': 'E-mail (SMTP)', 'sec.lookup': 'Callsign Lookup',
|
||||
'sec.bands': 'Bands', 'sec.modes': 'Modes & default RST', 'sec.cluster': 'DX Cluster',
|
||||
'sec.udp': 'Connections', 'sec.database': 'Database', 'sec.autostart': 'Autostart', 'sec.backup': 'Database backup', 'sec.uscounties': 'US Counties',
|
||||
'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',
|
||||
@@ -552,7 +562,17 @@ const fr: Dict = {
|
||||
'nav.user': 'Configuration utilisateur', 'nav.software': 'Configuration logicielle', 'nav.hardware': 'Configuration matérielle', 'nav.lists': 'Listes',
|
||||
'sec.station': 'Informations station', 'sec.profiles': 'Profils', 'sec.operating': "Conditions d'opération",
|
||||
'sec.confirmations': 'Confirmations', 'sec.external': 'Services externes',
|
||||
'sec.general': 'Général', 'sec.appearance': 'Apparence', 'appr.enable': 'Colorer les lignes entières selon le statut QSL', 'appr.enableHint': '(dans le tableau du log, comme Logger32)', 'appr.orderHint': "Un contact est souvent plusieurs de ces états à la fois — la première règle qui correspond décide de la couleur.", 'appr.ruleToSend': 'À envoyer', 'appr.ruleConfirmed': 'Confirmé', 'appr.ruleSent': 'QSL envoyée', 'appr.ruleWorked': 'Contacté, rien envoyé', 'appr.chQsl': 'QSL papier', 'appr.custom': 'Choisir une couleur', 'appr.style': 'Style', 'appr.styleBar': 'Barre à gauche', 'appr.styleTint': 'Ligne remplie', 'appr.styleBoth': 'Les deux', 'appr.intensity': 'Intensité', 'appr.bandmapLotw': 'Marquer les utilisateurs LoTW sur la band map', 'appr.bandmapLotwHint': '(le même badge L que la liste du cluster)', 'sec.email': 'E-mail (SMTP)', 'sec.lookup': "Recherche d'indicatif",
|
||||
'sec.general': 'Général', 'sec.appearance': 'Apparence', 'appr.enable': 'Colorer les lignes entières selon le statut QSL', 'appr.enableHint': '(dans le tableau du log, comme Logger32)', 'appr.orderHint': "Un contact est souvent plusieurs de ces états à la fois — la première règle qui correspond décide de la couleur.", 'appr.ruleToSend': 'À envoyer', 'appr.ruleConfirmed': 'Confirmé', 'appr.ruleSent': 'QSL envoyée', 'appr.ruleWorked': 'Contacté, rien envoyé', 'appr.chQsl': 'QSL papier', 'appr.custom': 'Choisir une couleur', 'appr.style': 'Style', 'appr.styleBar': 'Barre à gauche', 'appr.styleTint': 'Ligne remplie', 'appr.styleBoth': 'Les deux', 'appr.intensity': 'Intensité', 'appr.bandmapLotw': 'Marquer les utilisateurs LoTW sur la band map', 'appr.bandmapLotwHint': '(le même badge L que la liste du cluster)',
|
||||
'appr.matrixEnable': 'Choisir les couleurs de la matrice bandes/modes', 'appr.matrixHint': '(la grille PH/CW/DIG des Stats — décoché, chaque thème garde les siennes)',
|
||||
'appr.matrixSample': 'Aperçu', 'appr.matrixReset': 'Revenir aux couleurs du thème',
|
||||
// Légende de la matrice + noms des couleurs. Un seul jeu de libellés pour la
|
||||
// légende, les infobulles des cases et les sélecteurs de couleur.
|
||||
'mx.callConf': 'Indicatif confirmé', 'mx.callWork': 'Indicatif contacté', 'mx.dxConf': 'Entité confirmée',
|
||||
'mx.dxWork': 'Entité contactée', 'mx.none': 'Jamais contacté', 'mx.current': 'Saisie en cours',
|
||||
'mx.tipCallConf': 'Cet indicatif est confirmé', 'mx.tipCallWork': 'Cet indicatif est contacté (non confirmé)',
|
||||
'mx.tipDxConf': 'Entité confirmée (autre indicatif)', 'mx.tipDxWork': 'Entité contactée (autre indicatif)',
|
||||
'mx.tipNone': 'Jamais contacté', 'mx.tipClick': 'cliquer pour lister les QSO',
|
||||
'sec.email': 'E-mail (SMTP)', 'sec.lookup': "Recherche d'indicatif",
|
||||
'sec.bands': 'Bandes', 'sec.modes': 'Modes & RST par défaut', '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',
|
||||
'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',
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// Operator overrides for the band/mode matrix palette.
|
||||
//
|
||||
// Applied as inline custom properties on <html>, which is what lets them be
|
||||
// OVERRIDES rather than a palette: the twelve themes each define their own
|
||||
// --mx-* ramp in style.css, an inline value wins over all of them, and removing
|
||||
// it hands the colour straight back to the theme. Nothing has to know which
|
||||
// theme is active, and switching theme with overrides off is a clean revert.
|
||||
|
||||
export type MatrixColors = {
|
||||
enabled: boolean;
|
||||
call_confirmed: string;
|
||||
call_worked: string;
|
||||
entity_confirmed: string;
|
||||
entity_worked: string;
|
||||
not_worked: string;
|
||||
current_entry: string;
|
||||
};
|
||||
|
||||
// The six settings fields and the CSS custom property each one drives. Also the
|
||||
// display order — the same order the legend under the matrix reads in, so the
|
||||
// settings panel and the grid can never disagree about which green is which.
|
||||
export const MATRIX_VARS: { key: keyof Omit<MatrixColors, 'enabled'>; cssVar: string; label: string }[] = [
|
||||
{ key: 'call_confirmed', cssVar: '--mx-call-conf', label: 'mx.callConf' },
|
||||
{ key: 'call_worked', cssVar: '--mx-call-work', label: 'mx.callWork' },
|
||||
{ key: 'entity_confirmed', cssVar: '--mx-dx-conf', label: 'mx.dxConf' },
|
||||
{ key: 'entity_worked', cssVar: '--mx-dx-work', label: 'mx.dxWork' },
|
||||
{ key: 'not_worked', cssVar: '--mx-none', label: 'mx.none' },
|
||||
{ key: 'current_entry', cssVar: '--mx-cur', label: 'mx.current' },
|
||||
];
|
||||
|
||||
export const emptyMatrixColors = (): MatrixColors => ({
|
||||
enabled: false,
|
||||
call_confirmed: '', call_worked: '', entity_confirmed: '',
|
||||
entity_worked: '', not_worked: '', current_entry: '',
|
||||
});
|
||||
|
||||
// applyMatrixColors stamps (or clears) the overrides on <html>. Safe to call as
|
||||
// often as you like — it is the whole rendering path, so the settings panel uses
|
||||
// it for a live preview and the app uses it once at startup.
|
||||
export function applyMatrixColors(c?: MatrixColors | null): void {
|
||||
const root = document.documentElement;
|
||||
for (const { key, cssVar } of MATRIX_VARS) {
|
||||
const v = c?.enabled ? String(c[key] ?? '').trim() : '';
|
||||
if (v) root.style.setProperty(cssVar, v);
|
||||
else root.style.removeProperty(cssVar);
|
||||
}
|
||||
}
|
||||
|
||||
// effectiveMatrixColor reads what the matrix is ACTUALLY painting right now —
|
||||
// the override if there is one, else the active theme's value. It is what seeds
|
||||
// the colour pickers, so the operator starts from the colours in front of them
|
||||
// instead of from a hardcoded palette that may belong to a different theme.
|
||||
//
|
||||
// A custom property's computed value has its var() references substituted, so
|
||||
// --mx-cur resolves to the theme's --warning rather than to the literal text.
|
||||
export function effectiveMatrixColor(cssVar: string): string {
|
||||
try {
|
||||
const v = getComputedStyle(document.documentElement).getPropertyValue(cssVar).trim();
|
||||
// <input type="color"> only accepts #rrggbb. Anything else (a theme that
|
||||
// ever moves to oklch, an empty read during boot) falls back to mid grey
|
||||
// rather than silently resetting the picker to black.
|
||||
return /^#[0-9a-f]{6}$/i.test(v) ? v.toLowerCase() : '#808080';
|
||||
} catch {
|
||||
return '#808080';
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,12 @@
|
||||
--mx-dx-conf: #3730a3; /* entity confirmed*/
|
||||
--mx-dx-work: #a5b4fc; /* entity worked */
|
||||
--mx-none: #e7e5e4; /* never worked */
|
||||
/* Ring on the cell the operator is entering. Declared ONCE, on :root, and
|
||||
deliberately not repeated per theme: it follows --warning, which every theme
|
||||
already tunes to its own background. The token exists so the matrix ring can
|
||||
be recoloured on its own without dragging every other warning in the app
|
||||
with it (Appearance → matrix colours). */
|
||||
--mx-cur: var(--warning);
|
||||
|
||||
--scrollbar-thumb: #b8a880;
|
||||
--scrollbar-thumb-hover: #968455;
|
||||
@@ -974,6 +980,7 @@
|
||||
--color-mx-dx-conf: var(--mx-dx-conf);
|
||||
--color-mx-dx-work: var(--mx-dx-work);
|
||||
--color-mx-none: var(--mx-none);
|
||||
--color-mx-cur: var(--mx-cur);
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
|
||||
Vendored
+4
@@ -477,6 +477,8 @@ export function GetLogbookRevision():Promise<string>;
|
||||
|
||||
export function GetLookupSettings():Promise<main.LookupSettings>;
|
||||
|
||||
export function GetMatrixColors():Promise<main.MatrixColors>;
|
||||
|
||||
export function GetMySQLSettings():Promise<main.MySQLSettings>;
|
||||
|
||||
export function GetOfflineStatus():Promise<main.OfflineStatus>;
|
||||
@@ -949,6 +951,8 @@ export function SaveListsSettings(arg1:main.ListsSettings):Promise<void>;
|
||||
|
||||
export function SaveLookupSettings(arg1:main.LookupSettings):Promise<void>;
|
||||
|
||||
export function SaveMatrixColors(arg1:main.MatrixColors):Promise<void>;
|
||||
|
||||
export function SaveMySQLSettings(arg1:main.MySQLSettings):Promise<void>;
|
||||
|
||||
export function SaveOperatingAntenna(arg1:operating.Antenna):Promise<operating.Antenna>;
|
||||
|
||||
@@ -894,6 +894,10 @@ export function GetLookupSettings() {
|
||||
return window['go']['main']['App']['GetLookupSettings']();
|
||||
}
|
||||
|
||||
export function GetMatrixColors() {
|
||||
return window['go']['main']['App']['GetMatrixColors']();
|
||||
}
|
||||
|
||||
export function GetMySQLSettings() {
|
||||
return window['go']['main']['App']['GetMySQLSettings']();
|
||||
}
|
||||
@@ -1838,6 +1842,10 @@ export function SaveLookupSettings(arg1) {
|
||||
return window['go']['main']['App']['SaveLookupSettings'](arg1);
|
||||
}
|
||||
|
||||
export function SaveMatrixColors(arg1) {
|
||||
return window['go']['main']['App']['SaveMatrixColors'](arg1);
|
||||
}
|
||||
|
||||
export function SaveMySQLSettings(arg1) {
|
||||
return window['go']['main']['App']['SaveMySQLSettings'](arg1);
|
||||
}
|
||||
|
||||
@@ -2683,6 +2683,30 @@ export namespace main {
|
||||
this.cache_ttl_days = source["cache_ttl_days"];
|
||||
}
|
||||
}
|
||||
export class MatrixColors {
|
||||
enabled: boolean;
|
||||
call_confirmed: string;
|
||||
call_worked: string;
|
||||
entity_confirmed: string;
|
||||
entity_worked: string;
|
||||
not_worked: string;
|
||||
current_entry: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new MatrixColors(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.enabled = source["enabled"];
|
||||
this.call_confirmed = source["call_confirmed"];
|
||||
this.call_worked = source["call_worked"];
|
||||
this.entity_confirmed = source["entity_confirmed"];
|
||||
this.entity_worked = source["entity_worked"];
|
||||
this.not_worked = source["not_worked"];
|
||||
this.current_entry = source["current_entry"];
|
||||
}
|
||||
}
|
||||
|
||||
export class MySQLSettings {
|
||||
enabled: boolean;
|
||||
|
||||
Reference in New Issue
Block a user