chore: release v0.26.1
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user