feat: the header callsign is the station switcher; clock moves to the status bar

Switching station is the frequent act and editing a profile the rare one, but
the callsign opened the settings — so changing station took four clicks and a
scroll. It is now a dropdown of every profile: pick one and it activates. The
active one is ticked and disabled, and "Manage profiles…" stays at the bottom
for the rare case.

The list reloads on every profile change, so a profile added or renamed in the
settings appears without a restart.

The UTC clock leaves the header for the status bar, next to the database: in the
header it sat beside the frequency in the same weight and competed with it for
the eye, while being something you consult rather than watch.
This commit is contained in:
2026-07-28 14:25:06 +02:00
parent c3958be0b2
commit 5a6db2b656
3 changed files with 74 additions and 24 deletions
+6 -2
View File
@@ -10,7 +10,9 @@
"The diagnostic log no longer carries the FlexRadio meter readings, which drowned everything else.",
"OmniRig: a new \"VFO to read\" setting. OmniRig reports the active VFO from the rig file, and some files get it wrong — an IC-7610 file declared VFO B while the operator was on the main VFO, so OpsLog wrote to one VFO and read the other, and the frequency looked frozen. Force VFO A or B when that happens.",
"Bulk edit: fixed \"field is not bulk-editable\" on the QRZ.com received status and the confirmation dates added in 0.21.6 — they were declared in the wrong place and refused at Apply. Bulk-editing State and County, offered since the start, was refused the same way and now works.",
"QRZ.com confirmation download: the whole logbook was requested every time, which QRZ cuts short on a large log — the read stopped two thirds of the way through and the rest was never seen. Only the window is requested now. And the last-download date is no longer moved when the download was incomplete: it used to advance over records that had never been read, which skipped them for good."
"QRZ.com confirmation download: the whole logbook was requested every time, which QRZ cuts short on a large log — the read stopped two thirds of the way through and the rest was never seen. Only the window is requested now. And the last-download date is no longer moved when the download was incomplete: it used to advance over records that had never been read, which skipped them for good.",
"The callsign in the top bar is now the station switcher: click it and pick a profile to activate it. Managing profiles is still there, at the bottom of the list.",
"The UTC clock moved from the top bar to the status bar at the bottom, beside the database."
],
"fr": [
"Le constructeur de filtres reprend les mêmes noms de champs de confirmation que l'édition en lot, gagne le statut de réception QRZ.com manquant, et permet de filtrer sur les dates d'envoi et de réception (avant / après).",
@@ -20,7 +22,9 @@
"Le journal de diagnostic ne contient plus les mesures des instruments FlexRadio, qui noyaient tout le reste.",
"OmniRig : nouveau réglage « VFO à lire ». OmniRig indique le VFO actif d'après le fichier radio, et certains fichiers se trompent — un fichier IC-7610 déclarait le VFO B alors que l'opérateur était sur le VFO principal, si bien qu'OpsLog écrivait dans l'un et lisait l'autre, et la fréquence semblait figée. Forcez le VFO A ou B le cas échéant.",
"Édition en lot : correction du refus « field is not bulk-editable » sur le statut de réception QRZ.com et les dates de confirmation ajoutées en 0.21.6 — ils étaient déclarés au mauvais endroit et rejetés au moment d'appliquer. L'édition en lot de State et County, proposée depuis toujours, était refusée de la même façon et fonctionne désormais.",
"Téléchargement des confirmations QRZ.com : le carnet entier était demandé à chaque fois, ce que QRZ tronque sur un grand log — la lecture s'arrêtait aux deux tiers et le reste n'était jamais vu. Seule la fenêtre est désormais demandée. Et la date du dernier téléchargement n'avance plus lorsque le téléchargement est incomplet : elle passait par-dessus des enregistrements jamais lus, qui étaient alors ignorés définitivement."
"Téléchargement des confirmations QRZ.com : le carnet entier était demandé à chaque fois, ce que QRZ tronque sur un grand log — la lecture s'arrêtait aux deux tiers et le reste n'était jamais vu. Seule la fenêtre est désormais demandée. Et la date du dernier téléchargement n'avance plus lorsque le téléchargement est incomplet : elle passait par-dessus des enregistrements jamais lus, qui étaient alors ignorés définitivement.",
"L'indicatif en haut de la fenêtre est désormais le sélecteur de station : un clic, on choisit un profil et il devient actif. La gestion des profils reste accessible, en bas de la liste.",
"L'horloge UTC est passée de la barre du haut à la barre d'état en bas, à côté de la base de données."
]
},
{
+57 -11
View File
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Activity, AlertCircle, Antenna, Bell, CheckCircle2, Clock, CloudOff, Compass, Database, Ear, Eraser, Gauge, Hash, Loader2, Lock,
Activity, AlertCircle, Antenna, Bell, Check, CheckCircle2, ChevronDown, Clock, CloudOff, Compass, Database, Ear, Eraser, Gauge, Hash, Loader2, Lock,
Maximize2, Minimize2, Mic, MessageSquare, Pencil, Radio, RadioTower, RefreshCw, Satellite, Send, Settings, SlidersHorizontal, SpellCheck, Square, Terminal, Trash2, Unlock, X, Zap,
} from 'lucide-react';
@@ -45,7 +45,7 @@ import {
ChatAvailable, GetChatHistory, SendChatMessage, GetOnlineOperators,
QSOAudioBegin, QSOAudioCancel, QSOAudioRestart, QSOAudioResetClock,
GetAwardDefs,
GetUIPref, GetActiveProfile, QuitApp,
GetUIPref, GetActiveProfile, ListProfiles, ActivateProfile, QuitApp,
ReportLiveActivity, LiveLastQSOAgeSec,
GetAmpStatuses, AmpOperate,
GetFlexState, FlexAmpOperate,
@@ -57,6 +57,9 @@ import type { adif as adifModels, lookup as lookupModels, cat as catModels } fro
import type { QSOForm, WorkedBeforeView, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';
import { Menubar, type Menu } from '@/components/Menubar';
import {
DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator,
} from '@/components/ui/dropdown-menu';
import { APP_VERSION, APP_AUTHOR } from '@/version';
import { QSLManagerPanel } from '@/components/QSLManagerModal';
import { QslDesignerModal } from '@/components/qsl/QslDesignerModal';
@@ -2398,6 +2401,18 @@ export default function App() {
// every profile switch; the grids take it as their React key so they remount
// and re-read the now-correct per-profile layout.
const [activeProfileId, setActiveProfileId] = useState<number | null>(null);
// The station switcher in the header needs the whole list, not just the active
// one. Reloaded on every profile change so a profile renamed or added in the
// settings shows up without a restart.
const [profileList, setProfileList] = useState<{ id: number; callsign: string; name: string }[]>([]);
const loadProfileList = useCallback(() => {
ListProfiles().then((ps: any) => {
setProfileList((Array.isArray(ps) ? ps : []).map((p: any) => ({
id: p.id, callsign: p.callsign ?? '', name: p.name ?? '',
})));
}).catch(() => {});
}, []);
useEffect(() => { loadProfileList(); }, [loadProfileList]);
useEffect(() => {
GetActiveProfile().then((p: any) => {
if (p && p.id != null) { setGridPrefsProfile(p.id); setActiveProfileId(p.id); }
@@ -2420,7 +2435,7 @@ export default function App() {
// Re-scope the grid column layout BEFORE the grids remount (key change).
setGridPrefsProfile(id ?? null);
setActiveProfileId(typeof id === 'number' ? id : null);
loadStation(); loadLists(); loadCATCfg(); reloadWk(); loadMainPanes();
loadStation(); loadLists(); loadCATCfg(); reloadWk(); loadMainPanes(); loadProfileList();
// The chat is per shared logbook — clear the previous profile's messages
// and reload for the new logbook (or hide if it isn't a MySQL log).
setChatMsgs([]); chatSeen.current.clear(); setChatOnline([]); setChatUnread(0);
@@ -2428,7 +2443,7 @@ export default function App() {
setChatEpoch((e) => e + 1);
});
return () => { off(); };
}, [loadStation, loadLists, loadCATCfg, reloadWk, loadMainPanes]);
}, [loadStation, loadLists, loadCATCfg, reloadWk, loadMainPanes, loadProfileList]);
useEffect(() => {
(async () => {
await reloadWk();
@@ -4650,24 +4665,48 @@ export default function App() {
) : <span />}
</div>
<div className="flex items-center gap-1.5 font-mono text-xs text-muted-foreground px-2.5 py-1 bg-muted rounded-md border border-border/60">
<Clock className="size-3" />
{utcNow}<span className="text-[10px]">UTC</span>
</div>
<div className="flex items-center gap-3">
{station.callsign ? (
// Switching station is the frequent act; editing a profile is the
// rare one. The callsign used to open the settings, so changing
// station took four clicks and a scroll — it now IS the switcher,
// with the settings kept at the bottom of the list.
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
onClick={() => { setSettingsSection('profiles'); setShowSettings(true); }}
className="flex items-center gap-1.5 bg-accent border border-accent-foreground/20 text-accent-foreground px-3 py-1 rounded-full font-mono text-xs font-bold hover:bg-accent/80 transition-colors"
title="Click to switch / edit profiles"
title={t('prof.switchTitle')}
>
<Antenna className="size-3" />
{station.callsign}
{station.my_grid && (
<span className="bg-card/60 px-1.5 rounded text-[11px] text-primary">{station.my_grid}</span>
)}
<ChevronDown className="size-3 opacity-70" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-56">
{profileList.map((p) => (
<DropdownMenuItem
key={p.id}
disabled={p.id === activeProfileId}
onSelect={() => { if (p.id !== activeProfileId) ActivateProfile(p.id).catch((e: any) => setError(String(e?.message ?? e))); }}
>
<span className="flex items-center gap-2 min-w-0">
{p.id === activeProfileId
? <Check className="size-3.5 shrink-0 text-primary" />
: <span className="size-3.5 shrink-0" />}
<span className="font-mono font-semibold">{p.callsign || '—'}</span>
{p.name && <span className="text-muted-foreground truncate">{p.name}</span>}
</span>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={() => { setSettingsSection('profiles'); setShowSettings(true); }}>
{t('prof.manage')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<Button variant="outline" size="sm" onClick={() => setShowSettings(true)}>
<Settings className="size-3.5" /> Set station
@@ -5930,6 +5969,13 @@ export default function App() {
</>
)}
</div>
{/* UTC clock moved out of the header, where it competed with the
frequency for the eye. It belongs with the other passive
indicators. */}
<span className="inline-flex items-center gap-1 font-mono text-[11px] text-muted-foreground shrink-0" title="UTC">
<Clock className="size-3" />
{utcNow}<span className="text-[9px]">Z</span>
</span>
{dbConn && (
<button
type="button"
+2 -2
View File
@@ -40,7 +40,7 @@ const en: Dict = {
'cwd.tipOff': 'CW decoder · click to enable (decodes RX audio in CW mode)',
'tools.net': 'NET Control', 'tools.alerts': 'Alert management…', 'tools.contest': 'Contest mode',
'alert.tuneHint': 'Click to tune the rig to this spot (freq + mode) and fill the call', 'alert.dismiss': 'Dismiss', 'alert.pending': '{n} recent spot alert(s) — click to view', 'alert.noneShort': 'No recent alerts', 'alert.recent': 'Recent alerts', 'alert.clear': 'Clear',
'menu.help': 'Help', 'help.about': 'About OpsLog', 'help.donate': '♥ Support OpsLog (donate)', 'tools.duplicates': 'Find duplicates…',
'menu.help': 'Help', 'prof.switchTitle': 'Switch station profile', 'prof.manage': 'Manage profiles…', 'help.about': 'About OpsLog', 'help.donate': '♥ Support OpsLog (donate)', 'tools.duplicates': 'Find duplicates…',
'help.sendLog': 'Send log to F4BPO', 'help.sendLogBusy': 'Sending log…',
'help.sendLogOk': 'Log sent to F4BPO — thanks, 73!', 'help.sendLogFail': 'Could not send log: {err}',
// Duplicates modal
@@ -456,7 +456,7 @@ const fr: Dict = {
'cwd.tipOff': 'Décodeur CW · clic pour activer (décode laudio RX en mode CW)',
'tools.net': 'Contrôle de NET', 'tools.alerts': 'Gestion des alertes…', 'tools.contest': 'Mode contest',
'alert.tuneHint': 'Cliquer pour accorder la radio sur ce spot (fréq + mode) et remplir l\'indicatif', 'alert.dismiss': 'Fermer', 'alert.pending': '{n} alerte(s) de spot récente(s) — cliquer pour voir', 'alert.noneShort': 'Aucune alerte récente', 'alert.recent': 'Alertes récentes', 'alert.clear': 'Effacer',
'menu.help': 'Aide', 'help.about': 'À propos d\'OpsLog', 'help.donate': '♥ Soutenir OpsLog (don)', 'tools.duplicates': 'Trouver les doublons…',
'menu.help': 'Aide', 'prof.switchTitle': 'Changer de profil de station', 'prof.manage': 'Gérer les profils…', 'help.about': 'À propos d\'OpsLog', 'help.donate': '♥ Soutenir OpsLog (don)', 'tools.duplicates': 'Trouver les doublons…',
'help.sendLog': 'Envoyer le journal à F4BPO', 'help.sendLogBusy': 'Envoi du journal…',
'help.sendLogOk': 'Journal envoyé à F4BPO — merci, 73 !', 'help.sendLogFail': 'Échec de l\'envoi du journal : {err}',
'dup.title': 'Trouver les doublons', 'dup.scanning': 'Analyse du journal…', 'dup.none': 'Aucun doublon trouvé. 🎉',