diff --git a/app.go b/app.go index 7b4bb2b..3dd3665 100644 --- a/app.go +++ b/app.go @@ -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. diff --git a/appearance.go b/appearance.go index 9e2f584..5fe0513 100644 --- a/appearance.go +++ b/appearance.go @@ -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 { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 808e909..cc6c7da 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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(null); useEffect(() => { GetRowColors().then(setRowColors).catch(() => {}); }, [showSettings]); + // Band/mode matrix palette overrides (Settings → Appearance). Stamped onto + // 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 diff --git a/frontend/src/components/AppearancePanel.tsx b/frontend/src/components/AppearancePanel.tsx index 4d7751b..fd690c8 100644 --- a/frontend/src/components/AppearancePanel.tsx +++ b/frontend/src/components/AppearancePanel.tsx @@ -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 = { 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(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 ( +
+ + + {cfg.enabled && ( +
+
+ {MATRIX_VARS.map(({ key, cssVar, label }) => ( + + ))} +
+ + {/* The matrix as it will actually look: same tokens, same shapes. */} +
+ {t('appr.matrixSample')} + + + + + + +
+ + +
+ )} +
+ ); +} + export function AppearancePanel() { const { t } = useI18n(); const [cfg, setCfg] = useState(null); @@ -148,6 +257,8 @@ export function AppearancePanel() { ))} )} + + ); } diff --git a/frontend/src/components/BandSlotGrid.tsx b/frontend/src/components/BandSlotGrid.tsx index 8aa5675..ce7fa9e 100644 --- a/frontend/src/components/BandSlotGrid.tsx +++ b/frontend/src/components/BandSlotGrid.tsx @@ -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 = { 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 ( 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)} ))} diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index 31d40dc..d12b977 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -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', diff --git a/frontend/src/lib/matrixColors.ts b/frontend/src/lib/matrixColors.ts new file mode 100644 index 0000000..cc9e813 --- /dev/null +++ b/frontend/src/lib/matrixColors.ts @@ -0,0 +1,66 @@ +// Operator overrides for the band/mode matrix palette. +// +// Applied as inline custom properties on , 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; 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 . 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(); + // 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'; + } +} diff --git a/frontend/src/style.css b/frontend/src/style.css index b491fb2..d1bb2f4 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -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; diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index b83252d..979aa6c 100644 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -477,6 +477,8 @@ export function GetLogbookRevision():Promise; export function GetLookupSettings():Promise; +export function GetMatrixColors():Promise; + export function GetMySQLSettings():Promise; export function GetOfflineStatus():Promise; @@ -949,6 +951,8 @@ export function SaveListsSettings(arg1:main.ListsSettings):Promise; export function SaveLookupSettings(arg1:main.LookupSettings):Promise; +export function SaveMatrixColors(arg1:main.MatrixColors):Promise; + export function SaveMySQLSettings(arg1:main.MySQLSettings):Promise; export function SaveOperatingAntenna(arg1:operating.Antenna):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index 6210448..9167772 100644 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -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); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 4085a48..4ad431f 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -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;