feat(layout): drag the widgets into the order you want
A list in Appearance, in the row's own order, dragged to rearrange — with QSO entry and the F1-F5 panel at its head, locked. They are not in that row at all, and letting an operator push the thing they type into behind a rotator dial is not a preference, it is a trap. Implemented with flexbox ORDER rather than by moving the JSX: in a component this size, reordering the tree would have moved every condition, ref and hook with it. Each slot keeps its place in the source and receives an order property, so a widget switched off still holds its rank and returns where the operator left it. An unknown key from a later version joins the end rather than the front, and a key that no longer exists is dropped — an old preference can neither reorder a widget it has never heard of nor hide one. Opens 0.27.10.
This commit is contained in:
@@ -1,4 +1,14 @@
|
||||
[
|
||||
{
|
||||
"version": "0.27.10",
|
||||
"date": "",
|
||||
"en": [
|
||||
"Widget order (Settings → Appearance): the row to the right of the entry can be rearranged by dragging. QSO entry and the F1-F5 panel head the list, locked — they are not part of that row and nothing should be allowed to push what you type into behind a rotator dial. A widget you have switched off keeps its place and comes back where you left it, and the main view follows as you drag."
|
||||
],
|
||||
"fr": [
|
||||
"Ordre des widgets (Réglages → Apparence) : la rangée à droite de la saisie se réorganise par glisser-déposer. La saisie du QSO et le panneau F1-F5 ouvrent la liste, verrouillés — ils ne font pas partie de cette rangée, et rien ne doit pouvoir repousser ce dans quoi vous tapez derrière une boussole de rotor. Un widget désactivé garde sa place et revient là où vous l’aviez laissé, et la vue principale suit pendant que vous glissez."
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "0.27.9",
|
||||
"date": "",
|
||||
|
||||
+39
-12
@@ -59,6 +59,7 @@ import { Combobox } from '@/components/ui/combobox';
|
||||
import { applyAwardRefs, parseAwardRefs as parseManualRefs, spotRefList , withIOTARef, withRDARef } from '@/lib/awardRefs';
|
||||
import { ListRadios, SetActiveRadio } from '../wailsjs/go/main/App';
|
||||
import { formatDistance } from '@/lib/units';
|
||||
import { WIDGET_KEYS } from '@/components/AppearancePanel';
|
||||
import { bandForMHz } from '@/lib/bandplan';
|
||||
import { EventsOn, BrowserOpenURL, WindowMinimise, WindowToggleMaximise, WindowIsMaximised, Quit } from '../wailsjs/runtime/runtime';
|
||||
import type { adif as adifModels, lookup as lookupModels, cat as catModels } from '../wailsjs/go/models';
|
||||
@@ -761,6 +762,32 @@ export default function App() {
|
||||
// Several amps side by side make a wide widget, so an operator running two
|
||||
// picks the one he watches while transmitting. Declared here because the poll
|
||||
// below runs faster while the widget is open.
|
||||
// Widget order (Settings → Appearance). The row is a flex container, so the
|
||||
// ORDER property moves a widget without touching the tree: every condition,
|
||||
// every ref and every hook stays where it was, and a widget that is switched
|
||||
// off simply is not there to be ordered.
|
||||
//
|
||||
// QSO entry and the F1-F5 panel are not in this list on purpose — they are
|
||||
// not in this row at all, and an operator cannot be allowed to push the thing
|
||||
// they type into behind a rotator dial.
|
||||
const [widgetOrder, setWidgetOrder] = useState<string[]>(() => {
|
||||
try {
|
||||
const raw = localStorage.getItem('opslog.widgetOrder');
|
||||
const arr = raw ? JSON.parse(raw) : null;
|
||||
if (Array.isArray(arr) && arr.every((x) => typeof x === 'string')) return arr;
|
||||
} catch { /* corrupt pref → the default order */ }
|
||||
return [...WIDGET_KEYS];
|
||||
});
|
||||
useEffect(() => EventsOn('widgets:order', (keys: any) => {
|
||||
if (Array.isArray(keys)) setWidgetOrder(keys.filter((k: any) => typeof k === 'string'));
|
||||
}), []);
|
||||
// A key the saved order has never heard of (a widget added since) goes to the
|
||||
// end rather than to the front, where it would jump the queue on every
|
||||
// upgrade.
|
||||
const wOrder = (k: string) => {
|
||||
const i = widgetOrder.indexOf(k);
|
||||
return i < 0 ? WIDGET_KEYS.length + (WIDGET_KEYS as readonly string[]).indexOf(k) : i;
|
||||
};
|
||||
const [showAmpWidget, setShowAmpWidget] = useState(() => localStorage.getItem('opslog.showAmpWidget') !== '0');
|
||||
const [ampWidgetSel, setAmpWidgetSel] = useState(() => localStorage.getItem('opslog.ampSel.widget') || 'all');
|
||||
// Poll fast only while the amplifier widget is open: its meters must track TX
|
||||
@@ -7440,7 +7467,7 @@ export default function App() {
|
||||
{/* Multi-op "who's on air" widget: every operator on the shared logbook,
|
||||
their freq/mode (colour-coded) and OpsLog version. */}
|
||||
{showLiveStations && dbConn?.backend === 'mysql' && (
|
||||
<div className="w-[248px] shrink-0 min-h-0 relative">
|
||||
<div className="w-[248px] shrink-0 min-h-0 relative" style={{ order: wOrder('livestations') }}>
|
||||
<div className="absolute inset-0 flex flex-col min-h-0 rounded-xl border border-border bg-card shadow-sm overflow-hidden">
|
||||
<div className="flex items-center gap-1.5 px-3 h-8 border-b border-border shrink-0">
|
||||
<Radio className="size-3.5 text-primary" />
|
||||
@@ -7484,7 +7511,7 @@ export default function App() {
|
||||
// relative + absolute inner: the chat takes the row height (set by the
|
||||
// entry strip) WITHOUT its message list growing the row, like the
|
||||
// Stats panel. The list scrolls inside this fixed height.
|
||||
<div className="w-[280px] shrink-0 min-h-0 relative">
|
||||
<div className="w-[280px] shrink-0 min-h-0 relative" style={{ order: wOrder('chat') }}>
|
||||
<div className="absolute inset-0 flex flex-col min-h-0">
|
||||
<ChatPanel msgs={chatMsgs} online={chatOnline} myCall={station.callsign}
|
||||
onSend={chatSend} onClose={() => setChatOpen(false)} />
|
||||
@@ -7496,7 +7523,7 @@ export default function App() {
|
||||
controls column, so the widget is just the dial and needs only its
|
||||
width. */}
|
||||
{showRotor && (rotatorHeading.enabled || dxPath) && (
|
||||
<div className={cn('shrink-0 min-h-0', rotorCompact ? 'w-[196px]' : 'w-[320px]')}>
|
||||
<div className={cn('shrink-0 min-h-0', rotorCompact ? 'w-[196px]' : 'w-[320px]')} style={{ order: wOrder('rotor') }}>
|
||||
<RotorCompass
|
||||
presets={rotorCompact ? undefined : rotorPresets}
|
||||
onStop={rotorCompact ? undefined : () => { RotatorStop().then(pokeRotorHeading).catch((err) => setError(String(err?.message ?? err))); }}
|
||||
@@ -7516,7 +7543,7 @@ export default function App() {
|
||||
</div>
|
||||
)}
|
||||
{showMotorAnt && ubStatus.enabled && (
|
||||
<div className="w-[230px] shrink-0 min-h-0">
|
||||
<div className="w-[230px] shrink-0 min-h-0" style={{ order: wOrder('motorant') }}>
|
||||
<MotorAntennaWidget
|
||||
ant={ubStatus}
|
||||
refetch={pokeUbStatus}
|
||||
@@ -7527,7 +7554,7 @@ export default function App() {
|
||||
</div>
|
||||
)}
|
||||
{showAntGenius && agEnabled && (
|
||||
<div className="w-[230px] shrink-0 min-h-0">
|
||||
<div className="w-[230px] shrink-0 min-h-0" style={{ order: wOrder('antgenius') }}>
|
||||
<AntGeniusPanel
|
||||
status={agStatus}
|
||||
onActivate={agActivate}
|
||||
@@ -7540,7 +7567,7 @@ export default function App() {
|
||||
// One column per amplifier shown, so two amps stand side by side
|
||||
// rather than making the widget twice as tall as the dock row.
|
||||
<div className="shrink-0 min-h-0"
|
||||
style={{ width: `${Math.min(ampWidgetSel === 'all' ? ampSts.length : 1, 3) * 250 + 20}px` }}>
|
||||
style={{ width: `${Math.min(ampWidgetSel === 'all' ? ampSts.length : 1, 3) * 250 + 20}px`, order: wOrder('amp') }}>
|
||||
<AmpWidget
|
||||
amps={ampSts}
|
||||
flex={flexAmp}
|
||||
@@ -7550,7 +7577,7 @@ export default function App() {
|
||||
</div>
|
||||
)}
|
||||
{showTuner && tgEnabled && (
|
||||
<div className="w-[230px] shrink-0 min-h-0">
|
||||
<div className="w-[230px] shrink-0 min-h-0" style={{ order: wOrder('tuner') }}>
|
||||
<TunerGeniusPanel
|
||||
status={tgStatus}
|
||||
onTune={tgTune}
|
||||
@@ -7562,7 +7589,7 @@ export default function App() {
|
||||
</div>
|
||||
)}
|
||||
{showScp && scpEnabled && (
|
||||
<div className="w-[240px] shrink-0 min-h-0">
|
||||
<div className="w-[240px] shrink-0 min-h-0" style={{ order: wOrder('scp') }}>
|
||||
<ScpPanel
|
||||
result={scpResult}
|
||||
currentCall={callsign}
|
||||
@@ -7573,7 +7600,7 @@ export default function App() {
|
||||
</div>
|
||||
)}
|
||||
{chaseNewOn && showChaseNew && (
|
||||
<div className="w-[420px] shrink-0 min-h-0">
|
||||
<div className="w-[420px] shrink-0 min-h-0" style={{ order: wOrder('chasenew') }}>
|
||||
{/* Same reflex as clicking a cluster spot: the callsign into the
|
||||
entry, and the rig onto the frequency it was decoded on. */}
|
||||
<ChaseNewPanel onPick={(sp) => {
|
||||
@@ -7586,7 +7613,7 @@ export default function App() {
|
||||
</div>
|
||||
)}
|
||||
{dvkEnabled && (
|
||||
<div className="w-[320px] shrink-0 min-h-0">
|
||||
<div className="w-[320px] shrink-0 min-h-0" style={{ order: wOrder('dvk') }}>
|
||||
<DvkPanel
|
||||
messages={dvkMsgs}
|
||||
status={dvkStat}
|
||||
@@ -7602,7 +7629,7 @@ export default function App() {
|
||||
</div>
|
||||
)}
|
||||
{wkEnabled && (
|
||||
<div className="w-[380px] shrink-0 min-h-0">
|
||||
<div className="w-[380px] shrink-0 min-h-0" style={{ order: wOrder('winkeyer') }}>
|
||||
<WinkeyerPanel
|
||||
// A rig keyer has no serial status of its own: it is connected
|
||||
// exactly when its CAT backend is. Yaesu was missing from this
|
||||
@@ -7646,7 +7673,7 @@ export default function App() {
|
||||
{/* QRZ photo: when the keyer is open it sits to its right at natural
|
||||
(capped) width, shrinking the keyer panel rather than hiding it. */}
|
||||
{lookupResult?.image_url && (
|
||||
<div className={cn('min-w-0 flex items-center', (wkEnabled || dvkEnabled) ? 'shrink-0' : 'flex-1')}>
|
||||
<div className={cn('min-w-0 flex items-center', (wkEnabled || dvkEnabled) ? 'shrink-0' : 'flex-1')} style={{ order: wOrder('photo') }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => lookupResult.image_url && setPhotoModal(lookupResult.image_url)}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { EventsEmit } from '../../wailsjs/runtime/runtime';
|
||||
import { GripVertical, Lock } from 'lucide-react';
|
||||
import { GetMatrixColors, GetRowColors, SaveMatrixColors, SaveRowColors } from '../../wailsjs/go/main/App';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
@@ -290,6 +292,98 @@ export function AppearancePanel() {
|
||||
)}
|
||||
|
||||
<MatrixColorsSection />
|
||||
<WidgetOrderSection />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// The row of widgets to the right of the entry, in the order they appear.
|
||||
//
|
||||
// Flexbox does the moving in the main view — this list only decides the order
|
||||
// property each one gets. That is why a widget switched OFF still holds its
|
||||
// place here: it comes back where the operator left it rather than at the end.
|
||||
export const WIDGET_KEYS = [
|
||||
'livestations', 'chat', 'rotor', 'motorant', 'antgenius',
|
||||
'amp', 'tuner', 'scp', 'chasenew', 'dvk', 'winkeyer', 'photo',
|
||||
] as const;
|
||||
|
||||
const WIDGET_LABELS: Record<string, string> = {
|
||||
livestations: 'wo.livestations', chat: 'wo.chat', rotor: 'wo.rotor',
|
||||
motorant: 'wo.motorant', antgenius: 'wo.antgenius', amp: 'wo.amp',
|
||||
tuner: 'wo.tuner', scp: 'wo.scp', chasenew: 'wo.chasenew',
|
||||
dvk: 'wo.dvk', winkeyer: 'wo.winkeyer', photo: 'wo.photo',
|
||||
};
|
||||
|
||||
function readWidgetOrder(): string[] {
|
||||
try {
|
||||
const raw = localStorage.getItem('opslog.widgetOrder');
|
||||
const arr = raw ? JSON.parse(raw) : null;
|
||||
if (Array.isArray(arr)) {
|
||||
// A key from an older build that no longer exists is dropped; a widget
|
||||
// added since joins the end. An old preference can never hide a new one.
|
||||
const known = arr.filter((k: any) => (WIDGET_KEYS as readonly string[]).includes(k));
|
||||
return [...known, ...WIDGET_KEYS.filter((k) => !known.includes(k))];
|
||||
}
|
||||
} catch { /* corrupt pref → the default order */ }
|
||||
return [...WIDGET_KEYS];
|
||||
}
|
||||
|
||||
function WidgetOrderSection() {
|
||||
const { t } = useI18n();
|
||||
const [order, setOrder] = useState<string[]>(readWidgetOrder);
|
||||
const dragKey = useRef<string | null>(null);
|
||||
const [dragging, setDragging] = useState<string | null>(null);
|
||||
|
||||
const commit = (keys: string[]) => {
|
||||
setOrder(keys);
|
||||
try { localStorage.setItem('opslog.widgetOrder', JSON.stringify(keys)); } catch { /* private mode */ }
|
||||
// The main view listens: an order is meant to be watched as it is dragged,
|
||||
// not discovered after closing Preferences.
|
||||
EventsEmit('widgets:order', keys);
|
||||
};
|
||||
const moveTo = (from: string, to: string) => {
|
||||
if (from === to) return;
|
||||
const next = order.filter((k) => k !== from);
|
||||
const at = next.indexOf(to);
|
||||
next.splice(at < 0 ? next.length : at, 0, from);
|
||||
commit(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-semibold">{t('wo.title')}</h3>
|
||||
<p className="text-xs text-muted-foreground">{t('wo.hint')}</p>
|
||||
<div className="space-y-1 max-w-md">
|
||||
{/* The two that cannot move, shown so the order reads as the whole row
|
||||
rather than as a list that mysteriously starts at the third item. */}
|
||||
{['wo.entry', 'wo.details'].map((k) => (
|
||||
<div key={k}
|
||||
className="flex items-center gap-2 rounded-md border border-border/60 bg-muted/30 px-2 py-1.5 text-sm text-muted-foreground">
|
||||
<Lock className="size-3.5 shrink-0 opacity-60" />
|
||||
<span className="flex-1 min-w-0 truncate">{t(k)}</span>
|
||||
</div>
|
||||
))}
|
||||
{order.map((k) => (
|
||||
<div key={k}
|
||||
onDragOver={(e) => { if (dragKey.current) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; } }}
|
||||
onDrop={(e) => { if (dragKey.current) { e.preventDefault(); moveTo(dragKey.current, k); } }}
|
||||
className={cn('flex items-center gap-2 rounded-md border border-border bg-card px-2 py-1.5 text-sm',
|
||||
dragging === k && 'opacity-50')}>
|
||||
<span draggable
|
||||
onDragStart={(e) => { dragKey.current = k; setDragging(k); e.dataTransfer.effectAllowed = 'move'; }}
|
||||
onDragEnd={() => { dragKey.current = null; setDragging(null); }}
|
||||
title={t('wo.drag')}
|
||||
className="cursor-grab active:cursor-grabbing text-muted-foreground/50 hover:text-foreground">
|
||||
<GripVertical className="size-4" />
|
||||
</span>
|
||||
<span className="flex-1 min-w-0 truncate">{t(WIDGET_LABELS[k] ?? k)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button type="button" onClick={() => commit([...WIDGET_KEYS])}
|
||||
className="text-xs text-muted-foreground hover:text-foreground underline">
|
||||
{t('wo.reset')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ 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.zebra': 'Alternate row colours in Recent QSOs', 'appr.zebraHint': '(one row in two on a slightly different background — switch it off and every row is the same colour)', 'appr.zebraColor': 'Alternate row', 'appr.zebraAuto': 'Follow the theme', 'appr.bandmapLotw': 'Mark LoTW users on the band map', 'appr.bandmapLotwHint': '(the same L badge the cluster list uses)',
|
||||
'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', 'wo.title': 'Widget order', 'wo.hint': 'The row to the right of the entry, in the order it appears. Drag to rearrange. A widget you have switched off keeps its place and comes back where you left it.', 'wo.drag': 'Drag to move', 'wo.reset': 'Reset to the default order', 'wo.entry': 'QSO entry', 'wo.details': 'Extra information (F1-F5)', 'wo.livestations': 'Who is on air', 'wo.chat': 'Chat', 'wo.rotor': 'Rotator compass', 'wo.motorant': 'Motorised antenna', 'wo.antgenius': 'Antenna Genius', 'wo.amp': 'Amplifier', 'wo.tuner': 'Tuner Genius', 'wo.scp': 'Super Check Partial', 'wo.chasenew': 'Chase new', 'wo.dvk': 'Voice keyer', 'wo.winkeyer': 'CW keyer', 'wo.photo': 'Operator photo', 'appr.custom': 'Pick any colour', 'appr.style': 'Style', 'appr.styleBar': 'Left stripe', 'appr.styleTint': 'Filled row', 'appr.styleBoth': 'Both', 'appr.intensity': 'Strength', 'appr.zebra': 'Alternate row colours in Recent QSOs', 'appr.zebraHint': '(one row in two on a slightly different background — switch it off and every row is the same colour)', 'appr.zebraColor': 'Alternate row', 'appr.zebraAuto': 'Follow the theme', '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
|
||||
@@ -651,7 +651,7 @@ 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.zebra': 'Alterner la couleur des lignes dans QSO récents', 'appr.zebraHint': '(une ligne sur deux sur un fond légèrement différent — désactive et toutes les lignes ont la même couleur)', 'appr.zebraColor': 'Ligne alternée', 'appr.zebraAuto': 'Suivre le thème', 'appr.bandmapLotw': 'Marquer les utilisateurs LoTW sur la band map', 'appr.bandmapLotwHint': '(le même badge L que la liste du cluster)',
|
||||
'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', 'wo.title': 'Ordre des widgets', 'wo.hint': 'La rangée à droite de la saisie, dans son ordre d’affichage. Glissez pour réorganiser. Un widget désactivé garde sa place et revient là où vous l’aviez laissé.', 'wo.drag': 'Glisser pour déplacer', 'wo.reset': 'Rétablir l’ordre par défaut', 'wo.entry': 'Saisie du QSO', 'wo.details': 'Informations complémentaires (F1-F5)', 'wo.livestations': 'Qui est à l’air', 'wo.chat': 'Chat', 'wo.rotor': 'Boussole rotor', 'wo.motorant': 'Antenne motorisée', 'wo.antgenius': 'Antenna Genius', 'wo.amp': 'Amplificateur', 'wo.tuner': 'Tuner Genius', 'wo.scp': 'Super Check Partial', 'wo.chasenew': 'Chase new', 'wo.dvk': 'Voice keyer', 'wo.winkeyer': 'Manipulateur CW', 'wo.photo': 'Photo de l’opérateur', 'appr.custom': 'Choisir une couleur', 'appr.style': 'Style', 'appr.styleBar': 'Barre à gauche', 'appr.styleTint': 'Ligne remplie', 'appr.styleBoth': 'Les deux', 'appr.intensity': 'Intensité', 'appr.zebra': 'Alterner la couleur des lignes dans QSO récents', 'appr.zebraHint': '(une ligne sur deux sur un fond légèrement différent — désactive et toutes les lignes ont la même couleur)', 'appr.zebraColor': 'Ligne alternée', 'appr.zebraAuto': 'Suivre le thème', '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
|
||||
|
||||
Reference in New Issue
Block a user