Fix equipment profiles: allow selecting active setup and renaming default
- All profiles (including the default AT71+ATR2600C) now show Edit and Use buttons - Active profile tracked in localStorage via astronome_active_profile - Default profile is seeded from localStorage on first load so it can be renamed - Delete button only shown when more than one profile exists Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,15 +3,14 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { api } from '../api';
|
import { api } from '../api';
|
||||||
import type { HorizonPoint } from '../api/types';
|
import type { HorizonPoint } from '../api/types';
|
||||||
|
|
||||||
// Current hardcoded setup constants (from config.rs)
|
const DEFAULT_PROFILE: EquipProfile = {
|
||||||
const CURRENT_SETUP = {
|
id: 'default',
|
||||||
name: 'AT71 + ATR2600C (current)',
|
name: 'AT71 + ATR2600C',
|
||||||
focal_mm: 490,
|
focal_mm: 490,
|
||||||
aperture_mm: 71,
|
aperture_mm: 71,
|
||||||
pixel_um: 3.76,
|
pixel_um: 3.76,
|
||||||
res_x: 6248,
|
res_x: 6248,
|
||||||
res_y: 4176,
|
res_y: 4176,
|
||||||
active: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface EquipProfile {
|
interface EquipProfile {
|
||||||
@@ -24,9 +23,18 @@ interface EquipProfile {
|
|||||||
res_y: number;
|
res_y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadProfiles(): EquipProfile[] {
|
||||||
|
try {
|
||||||
|
const stored = JSON.parse(localStorage.getItem('astronome_equip_profiles') ?? '[]') as EquipProfile[];
|
||||||
|
// Ensure the default profile is always present
|
||||||
|
if (!stored.find(p => p.id === 'default')) {
|
||||||
|
return [DEFAULT_PROFILE, ...stored];
|
||||||
|
}
|
||||||
|
return stored;
|
||||||
|
} catch { return [DEFAULT_PROFILE]; }
|
||||||
|
}
|
||||||
|
|
||||||
function calcProfile(p: { focal_mm: number; aperture_mm: number; pixel_um: number; res_x: number; res_y: number }) {
|
function calcProfile(p: { focal_mm: number; aperture_mm: number; pixel_um: number; res_x: number; res_y: number }) {
|
||||||
const plate_scale = (206.265 * p.pixel_um) / (p.focal_mm * 1000 / 1000);
|
|
||||||
// plate_scale in arcsec/px: (206265 * pixel_size_um / 1000) / focal_mm
|
|
||||||
const ps = (206.265 * p.pixel_um / 1000) / p.focal_mm * 1000;
|
const ps = (206.265 * p.pixel_um / 1000) / p.focal_mm * 1000;
|
||||||
const fov_w_deg = (ps * p.res_x) / 3600;
|
const fov_w_deg = (ps * p.res_x) / 3600;
|
||||||
const fov_h_deg = (ps * p.res_y) / 3600;
|
const fov_h_deg = (ps * p.res_y) / 3600;
|
||||||
@@ -39,11 +47,10 @@ function calcProfile(p: { focal_mm: number; aperture_mm: number; pixel_um: numbe
|
|||||||
}
|
}
|
||||||
|
|
||||||
function EquipmentProfiles() {
|
function EquipmentProfiles() {
|
||||||
const [profiles, setProfiles] = useState<EquipProfile[]>(() => {
|
const [profiles, setProfiles] = useState<EquipProfile[]>(loadProfiles);
|
||||||
try {
|
const [activeId, setActiveId] = useState<string>(() =>
|
||||||
return JSON.parse(localStorage.getItem('astronome_equip_profiles') ?? '[]');
|
localStorage.getItem('astronome_active_profile') ?? 'default'
|
||||||
} catch { return []; }
|
);
|
||||||
});
|
|
||||||
const [editing, setEditing] = useState<EquipProfile | null>(null);
|
const [editing, setEditing] = useState<EquipProfile | null>(null);
|
||||||
const [adding, setAdding] = useState(false);
|
const [adding, setAdding] = useState(false);
|
||||||
const [form, setForm] = useState({ name: '', focal_mm: 490, aperture_mm: 71, pixel_um: 3.76, res_x: 6248, res_y: 4176 });
|
const [form, setForm] = useState({ name: '', focal_mm: 490, aperture_mm: 71, pixel_um: 3.76, res_x: 6248, res_y: 4176 });
|
||||||
@@ -52,6 +59,10 @@ function EquipmentProfiles() {
|
|||||||
localStorage.setItem('astronome_equip_profiles', JSON.stringify(profiles));
|
localStorage.setItem('astronome_equip_profiles', JSON.stringify(profiles));
|
||||||
}, [profiles]);
|
}, [profiles]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('astronome_active_profile', activeId);
|
||||||
|
}, [activeId]);
|
||||||
|
|
||||||
const saveProfile = () => {
|
const saveProfile = () => {
|
||||||
if (!form.name.trim()) return;
|
if (!form.name.trim()) return;
|
||||||
if (editing) {
|
if (editing) {
|
||||||
@@ -65,7 +76,9 @@ function EquipmentProfiles() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteProfile = (id: string) => {
|
const deleteProfile = (id: string) => {
|
||||||
|
if (profiles.length <= 1) return;
|
||||||
setProfiles(ps => ps.filter(p => p.id !== id));
|
setProfiles(ps => ps.filter(p => p.id !== id));
|
||||||
|
if (activeId === id) setActiveId(profiles.find(p => p.id !== id)?.id ?? 'default');
|
||||||
};
|
};
|
||||||
|
|
||||||
const startEdit = (p: EquipProfile) => {
|
const startEdit = (p: EquipProfile) => {
|
||||||
@@ -74,9 +87,6 @@ function EquipmentProfiles() {
|
|||||||
setAdding(false);
|
setAdding(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const current = calcProfile(CURRENT_SETUP);
|
|
||||||
const allProfiles = [{ ...CURRENT_SETUP, id: '__current__' }, ...profiles];
|
|
||||||
|
|
||||||
const fieldStyle: React.CSSProperties = {
|
const fieldStyle: React.CSSProperties = {
|
||||||
background: 'var(--bg-void)', border: '1px solid var(--border)', borderRadius: 3,
|
background: 'var(--bg-void)', border: '1px solid var(--border)', borderRadius: 3,
|
||||||
color: 'var(--text-hi)', fontFamily: 'var(--font-mono)', fontSize: 12,
|
color: 'var(--text-hi)', fontFamily: 'var(--font-mono)', fontSize: 12,
|
||||||
@@ -90,19 +100,19 @@ function EquipmentProfiles() {
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, maxWidth: 640 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, maxWidth: 640 }}>
|
||||||
{allProfiles.map(p => {
|
{profiles.map(p => {
|
||||||
const calc = calcProfile(p);
|
const calc = calcProfile(p);
|
||||||
const isCurrent = p.id === '__current__';
|
const isActive = p.id === activeId;
|
||||||
return (
|
return (
|
||||||
<div key={p.id} style={{
|
<div key={p.id} style={{
|
||||||
background: 'var(--bg-panel)', border: `1px solid ${isCurrent ? 'var(--amber-dim)' : 'var(--border)'}`,
|
background: 'var(--bg-panel)', border: `1px solid ${isActive ? 'var(--amber-dim)' : 'var(--border)'}`,
|
||||||
borderRadius: 6, padding: '12px 16px',
|
borderRadius: 6, padding: '12px 16px',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 }}>
|
||||||
<div>
|
<div style={{ minWidth: 0 }}>
|
||||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, color: isCurrent ? 'var(--amber)' : 'var(--text-hi)', fontWeight: 600 }}>
|
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, color: isActive ? 'var(--amber)' : 'var(--text-hi)', fontWeight: 600 }}>
|
||||||
{p.name}
|
{p.name}
|
||||||
{isCurrent && <span style={{ marginLeft: 8, fontSize: 10, color: 'var(--amber)', background: 'var(--amber-glow)', padding: '1px 6px', borderRadius: 3 }}>ACTIVE</span>}
|
{isActive && <span style={{ marginLeft: 8, fontSize: 10, color: 'var(--amber)', background: 'var(--amber-glow)', padding: '1px 6px', borderRadius: 3 }}>ACTIVE</span>}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-lo)', marginTop: 4 }}>
|
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-lo)', marginTop: 4 }}>
|
||||||
{p.focal_mm}mm {calc.focal_ratio} · {p.aperture_mm}mm aperture · {p.pixel_um}μm px · {p.res_x}×{p.res_y}
|
{p.focal_mm}mm {calc.focal_ratio} · {p.aperture_mm}mm aperture · {p.pixel_um}μm px · {p.res_x}×{p.res_y}
|
||||||
@@ -112,18 +122,23 @@ function EquipmentProfiles() {
|
|||||||
{' · '}FOV: <strong style={{ color: 'var(--teal)' }}>{calc.fov_w}</strong>
|
{' · '}FOV: <strong style={{ color: 'var(--teal)' }}>{calc.fov_w}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!isCurrent && (
|
<div style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
{!isActive && (
|
||||||
<button onClick={() => startEdit(p as EquipProfile)} style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--blue)', background: 'none', border: '1px solid var(--border)', borderRadius: 3, padding: '3px 8px', cursor: 'pointer' }}>
|
<button onClick={() => setActiveId(p.id)} style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--amber)', background: 'var(--amber-glow)', border: '1px solid var(--amber-dim)', borderRadius: 3, padding: '3px 8px', cursor: 'pointer', whiteSpace: 'nowrap' }}>
|
||||||
|
Use
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button onClick={() => startEdit(p)} style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--blue)', background: 'none', border: '1px solid var(--border)', borderRadius: 3, padding: '3px 8px', cursor: 'pointer' }}>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
|
{profiles.length > 1 && (
|
||||||
<button onClick={() => deleteProfile(p.id)} style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--danger)', background: 'none', border: '1px solid var(--border)', borderRadius: 3, padding: '3px 8px', cursor: 'pointer' }}>
|
<button onClick={() => deleteProfile(p.id)} style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--danger)', background: 'none', border: '1px solid var(--border)', borderRadius: 3, padding: '3px 8px', cursor: 'pointer' }}>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user