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:
+66
-20
@@ -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 ? (
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
</button>
|
||||
// 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
|
||||
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={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"
|
||||
|
||||
Reference in New Issue
Block a user