Split settings into tabs; exposure and focus on the home screen
Some checks failed
CI / build-and-deploy (push) Has been cancelled
Some checks failed
CI / build-and-deploy (push) Has been cancelled
Home screen gains shutter, aperture and ISO as dropdowns plus a focus stepper (three nudge sizes each way, and AF) where the body drives focus over PTP. Those three drop out of the read-only strip rather than being shown twice. Everything is built from what the camera actually reports, so a body without one of these controls doesn't get it rather than showing something that silently fails. The 450D marks shutter and aperture readonly unless the mode dial is somewhere they apply, so the dropdowns disable themselves and say why. Focus uses the EOS manualfocusdrive steps and autofocusdrive, and warns when live view is off, which Canon bodies generally require for focus commands. The settings drawer is now tabbed: app settings stay on the first tab, and each config section the camera reports gets its own. Empty sections are dropped, and a selected section that disappears falls back to the first tab. Tabs wrap rather than scroll - a scrolled-off tab is an undiscoverable one. Reverts the automatic camera search added in the previous commit, restoring the Select camera button. Auto-connect could park with no way to reach the device chooser: WebUSB permissions are per-origin, so a grant on localhost doesn't carry to the deployed copy, and a camera that's asleep or held by another app never arrives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QK7PKpRVoy69Fpa32R8abb
This commit is contained in:
@@ -70,6 +70,31 @@ export function savePrefs(prefs) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The camera's top-level config sections, each of which becomes a tab.
|
||||
* Empty ones are dropped so we don't offer a tab onto nothing.
|
||||
*
|
||||
* @param {import('web-gphoto2').Config | undefined} config
|
||||
* @returns {(import('web-gphoto2').Config & { type: 'section', children: any })[]}
|
||||
*/
|
||||
function cameraSections(config) {
|
||||
if (!config || config.type !== 'window') return [];
|
||||
return /** @type {any} */ (Object.values(config.children).filter(
|
||||
child =>
|
||||
(child.type === 'section' || child.type === 'window') &&
|
||||
Object.keys(child.children).length > 0
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Section labels are all "Camera Actions", "Camera Settings", … - the prefix
|
||||
* is dead weight in a tab that's already inside the camera's settings.
|
||||
* @param {string} label
|
||||
*/
|
||||
function tabLabel(label) {
|
||||
return label.replace(/^camera\s+/i, '') || label;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ label: string, hint?: string, children?: any }} props
|
||||
*/
|
||||
@@ -107,6 +132,9 @@ function Row({ label, hint, children }) {
|
||||
* }>
|
||||
*/
|
||||
export class SettingsDrawer extends Component {
|
||||
/** Selected tab: 'app', or the gphoto2 name of a config section. */
|
||||
state = { tab: 'app' };
|
||||
|
||||
#onKeyDown = (/** @type {KeyboardEvent} */ e) => {
|
||||
if (e.key === 'Escape' && this.props.open) this.props.onClose();
|
||||
};
|
||||
@@ -135,6 +163,17 @@ export class SettingsDrawer extends Component {
|
||||
testVoice
|
||||
} = props;
|
||||
|
||||
// One tab per top-level section the camera reports, so the config tree
|
||||
// stops being one enormous scroll.
|
||||
let sections = cameraSections(config);
|
||||
// The chosen section can vanish if the camera is swapped or reports
|
||||
// differently after a change; fall back rather than render nothing.
|
||||
let tab =
|
||||
this.state.tab !== 'app' && !sections.some(s => s.name === this.state.tab)
|
||||
? 'app'
|
||||
: this.state.tab;
|
||||
let selected = sections.find(s => s.name === tab);
|
||||
|
||||
return h(
|
||||
Fragment,
|
||||
null,
|
||||
@@ -160,6 +199,32 @@ export class SettingsDrawer extends Component {
|
||||
'✕'
|
||||
)
|
||||
),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'tab-bar' },
|
||||
h(
|
||||
'button',
|
||||
{
|
||||
type: 'button',
|
||||
class: `tab ${tab === 'app' ? 'active' : ''}`,
|
||||
onclick: () => this.setState({ tab: 'app' })
|
||||
},
|
||||
'App'
|
||||
),
|
||||
sections.map(section =>
|
||||
h(
|
||||
'button',
|
||||
{
|
||||
key: section.name,
|
||||
type: 'button',
|
||||
class: `tab ${tab === section.name ? 'active' : ''}`,
|
||||
title: section.label,
|
||||
onclick: () => this.setState({ tab: section.name })
|
||||
},
|
||||
tabLabel(section.label)
|
||||
)
|
||||
)
|
||||
),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'drawer-body' },
|
||||
@@ -171,6 +236,27 @@ export class SettingsDrawer extends Component {
|
||||
)
|
||||
: undefined,
|
||||
|
||||
selected
|
||||
? h(
|
||||
'form',
|
||||
{
|
||||
class: 'pure-form pure-form-aligned',
|
||||
onSubmit: e => e.preventDefault()
|
||||
},
|
||||
// The section's own children, not the section node itself -
|
||||
// the tab already names it, so a fieldset around it is noise.
|
||||
Object.values(selected.children).map(child =>
|
||||
h(Widget, { key: child.name, config: child, setValue })
|
||||
)
|
||||
)
|
||||
: undefined,
|
||||
|
||||
tab !== 'app'
|
||||
? undefined
|
||||
: h(
|
||||
Fragment,
|
||||
null,
|
||||
|
||||
h(
|
||||
'section',
|
||||
null,
|
||||
@@ -392,22 +478,14 @@ export class SettingsDrawer extends Component {
|
||||
: h(
|
||||
'p',
|
||||
{ class: 'notice' },
|
||||
'This camera does not expose a bulb or eosremoterelease control, so timed long exposures are unavailable. Use the shutter speed setting below instead (up to 30s on the 450D).'
|
||||
'This camera does not expose a bulb or eosremoterelease control, so timed long exposures are unavailable. Use the shutter speed setting instead (up to 30s on the 450D).'
|
||||
)
|
||||
),
|
||||
|
||||
h(
|
||||
'section',
|
||||
null,
|
||||
h('h3', null, 'Camera'),
|
||||
config
|
||||
? h(
|
||||
'form',
|
||||
{ class: 'pure-form pure-form-aligned', onSubmit: e => e.preventDefault() },
|
||||
h(Widget, { config, setValue })
|
||||
)
|
||||
: h('p', { class: 'notice' }, 'Reading camera configuration…')
|
||||
)
|
||||
!config
|
||||
? h('p', { class: 'notice' }, 'Reading camera configuration…')
|
||||
: undefined
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user