chore: release v0.26.1

This commit is contained in:
2026-08-21 01:43:14 +02:00
parent e3b7a35e2c
commit e6889caae9
17 changed files with 472 additions and 95 deletions
+68 -32
View File
@@ -168,7 +168,7 @@ interface Props {
initialSection?: string;
onClose: () => void;
onSaved: () => void;
onMainPaneChanged?: (side: 'left' | 'right', value: string) => void; // live Main-view layout update
onMainPaneChanged?: (side: 'left' | 'right' | 'p3' | 'p4' | 'layout', value: string) => void; // live Main-view layout update
flexAvailable?: boolean; // CAT backend is FlexRadio → offer it as a Main pane
icomAvailable?: boolean; // CAT backend is Icom → offer the Icom console as a Main pane
yaesuAvailable?: boolean; // CAT backend is Yaesu → offer the Yaesu console as a Main pane
@@ -1029,15 +1029,16 @@ function RelayAutoPanel() {
);
}
// MainViewPanes lets the operator choose what the Main tab's left and right
// panes show, independently: the great-circle map, the locator street map, the
// cluster grid or the worked-before grid. Per-profile (stored via SetUIPref,
// which is profile-prefixed). Self-contained so it owns its async-loaded state.
// MainViewPanes lets the operator choose what the Main tab shows, in up to four
// panes. The first two are the view; the third and fourth can be left empty, and
// an empty one is not drawn at all. Per-profile (stored via SetUIPref, which is
// profile-prefixed). Self-contained so it owns its async-loaded state.
const MAIN_PANE_VALUES = ['map1', 'map2', 'cluster', 'worked', 'recent', 'netcontrol', 'decodes'];
function MainViewPanes({ onChanged, flexAvailable, icomAvailable, yaesuAvailable }: { onChanged?: (side: 'left' | 'right', value: string) => void; flexAvailable?: boolean; icomAvailable?: boolean; yaesuAvailable?: boolean }) {
const PANE_NONE = 'none';
function MainViewPanes({ onChanged, flexAvailable, icomAvailable, yaesuAvailable }: { onChanged?: (side: 'left' | 'right' | 'p3' | 'p4' | 'layout', value: string) => void; flexAvailable?: boolean; icomAvailable?: boolean; yaesuAvailable?: boolean }) {
const { t } = useI18n();
const [left, setLeft] = useState('map1');
const [right, setRight] = useState('map2');
const [panes, setPanes] = useState<Record<string, string>>({ left: 'map1', right: 'map2', p3: PANE_NONE, p4: PANE_NONE });
const [layout, setLayout] = useState('quad');
// Radio-control panes are only offered when that CAT backend is active. Sorted A→Z.
const options = [
...MAIN_PANE_VALUES,
@@ -1046,42 +1047,77 @@ function MainViewPanes({ onChanged, flexAvailable, icomAvailable, yaesuAvailable
...(yaesuAvailable ? ['yaesu'] : []),
].map((value) => ({ value, label: t(`settings.pane.${value}`) }))
.sort((a, b) => a.label.localeCompare(b.label));
const KEYS: Record<string, string> = { left: 'mainPaneLeft', right: 'mainPaneRight', p3: 'mainPane3', p4: 'mainPane4' };
useEffect(() => {
const valid = (v: string) => v === 'flex' || v === 'icom' || v === 'yaesu' || MAIN_PANE_VALUES.includes(v);
Promise.all([GetUIPref('mainPaneLeft').catch(() => ''), GetUIPref('mainPaneRight').catch(() => '')])
.then(([l, r]) => { if (valid(l)) setLeft(l); if (valid(r)) setRight(r); });
Promise.all([
...Object.values(KEYS).map((k) => GetUIPref(k).catch(() => '')),
GetUIPref('mainPaneLayout').catch(() => ''),
]).then(([l, r, p3, p4, lay]) => {
setPanes({
left: valid(l) ? l : 'map1',
right: valid(r) ? r : 'map2',
p3: valid(p3) ? p3 : PANE_NONE,
p4: valid(p4) ? p4 : PANE_NONE,
});
if (lay === 'cols' || lay === 'quad') setLayout(lay);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const pick = (side: 'left' | 'right', v: string) => {
if (side === 'left') setLeft(v); else setRight(v);
const pick = (side: string, v: string) => {
setPanes((p) => ({ ...p, [side]: v }));
// Persist (per-profile) AND tell the parent the new value directly, so the
// Main view updates from the chosen value — never a stale DB re-read.
SetUIPref(side === 'left' ? 'mainPaneLeft' : 'mainPaneRight', v).catch(() => {});
onChanged?.(side, v);
SetUIPref(KEYS[side], v).catch(() => {});
onChanged?.(side as any, v);
};
const pickLayout = (v: string) => {
setLayout(v);
SetUIPref('mainPaneLayout', v).catch(() => {});
onChanged?.('layout', v);
};
const filled = ['left', 'right', 'p3', 'p4'].filter((k) => panes[k] !== PANE_NONE).length;
const SIDES = [
{ key: 'left', label: t('settings.leftPane'), canBeEmpty: false },
{ key: 'right', label: t('settings.rightPane'), canBeEmpty: false },
{ key: 'p3', label: t('settings.thirdPane'), canBeEmpty: true },
{ key: 'p4', label: t('settings.fourthPane'), canBeEmpty: true },
];
return (
<div className="border-t border-border/60 pt-4 space-y-2">
<h4 className="text-sm font-semibold text-foreground">{t('settings.mainView')}</h4>
<p className="text-xs text-muted-foreground">{t('settings.mainViewHint')}</p>
<div className="grid grid-cols-2 gap-3 max-w-xl">
<label className="flex flex-col gap-1 text-xs">
<span className="text-muted-foreground">{t('settings.leftPane')}</span>
<Select value={left} onValueChange={(v) => pick('left', v)}>
<SelectTrigger className="h-8 text-xs"><SelectValue /></SelectTrigger>
<SelectContent>
{options.map((o) => <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>)}
</SelectContent>
</Select>
</label>
<label className="flex flex-col gap-1 text-xs">
<span className="text-muted-foreground">{t('settings.rightPane')}</span>
<Select value={right} onValueChange={(v) => pick('right', v)}>
<SelectTrigger className="h-8 text-xs"><SelectValue /></SelectTrigger>
<SelectContent>
{options.map((o) => <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>)}
</SelectContent>
</Select>
</label>
{SIDES.map((sd) => (
<label key={sd.key} className="flex flex-col gap-1 text-xs">
<span className="text-muted-foreground">{sd.label}</span>
<Select value={panes[sd.key]} onValueChange={(v) => pick(sd.key, v)}>
<SelectTrigger className="h-8 text-xs"><SelectValue /></SelectTrigger>
<SelectContent>
{/* Only the third and fourth may be empty: the first two ARE the
Main view, and a tab with nothing in it is not a layout. */}
{sd.canBeEmpty && <SelectItem value={PANE_NONE}>{t('settings.paneNone')}</SelectItem>}
{options.map((o) => <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>)}
</SelectContent>
</Select>
</label>
))}
</div>
{/* Only asked when it can matter. Three panes are columns and nothing
else; four are the only case with two honest answers. */}
{filled === 4 && (
<div className="flex items-center gap-2 pt-1">
<span className="text-xs text-muted-foreground">{t('settings.paneLayout')}</span>
<div className="inline-flex rounded-md border border-border overflow-hidden text-xs">
{[{ v: 'quad', l: t('settings.paneQuad') }, { v: 'cols', l: t('settings.paneCols') }].map((o) => (
<button key={o.v} type="button" onClick={() => pickLayout(o.v)}
className={cn('px-2.5 py-1', layout === o.v ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted')}>
{o.l}
</button>
))}
</div>
</div>
)}
</div>
);
}