From baaa0b547266567253b1c4250338b4c5ce555ab5 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 2 Aug 2026 09:53:24 +0100 Subject: [PATCH] Add fullscreen preview, PWA support, and connect without a button Fullscreen: a button on the preview pane fullscreens the live view, carrying the countdown over as an overlay - fullscreen hides the entire control column, so without it you're left staring at a picture with no idea when the next frame lands. Wake lock: held from app start until you switch it off, rather than only for the duration of a sequence. You're usually mid-setup when the display would otherwise sleep. PWA: manifest, icons and a service worker, so it installs to a standalone window and runs with no network. That matters more here than for most web apps - the camera is on a USB cable, so this is fully functional in a field with no signal. The worker precaches the pinned unpkg dependencies at install rather than leaving them to the runtime cache. On a first visit the worker isn't controlling the page yet, so the app's own imports go straight to the network and never reach the fetch handler; without the precache it looked cached but died offline on its imports. Our own files are network-first so a redeploy always wins, and the version-pinned CDN files are cache-first. Connecting: no connect button. The app reaches for the camera on load, retries every 1.5s, and listens for USB connect events, so switching the camera on mid-wait attaches it with no click or reload. The exception is a browser that has never been granted access to the device: Chrome will not open its WebUSB chooser outside a user gesture, so that case still shows a one-off prompt. After that the permission is remembered and getDevices() finds the camera with no interaction. tsconfig excludes sw.js, which runs in ServiceWorkerGlobalScope and reports every worker global as undefined when checked against the DOM lib. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QK7PKpRVoy69Fpa32R8abb --- README.md | 5 +- examples/preact/build-dist.sh | 7 +- examples/preact/home.js | 89 ++++++++------ examples/preact/icon-192.png | Bin 0 -> 8710 bytes examples/preact/icon-512.png | Bin 0 -> 23297 bytes examples/preact/icon-maskable-512.png | Bin 0 -> 19898 bytes examples/preact/index.html | 95 +++++++++++++++ examples/preact/index.js | 162 ++++++++++++++++++++++---- examples/preact/manifest.webmanifest | 22 ++++ examples/preact/settings.js | 2 +- examples/preact/sw.js | 147 +++++++++++++++++++++++ examples/preact/tsconfig.json | 4 +- examples/preact/types.d.ts | 10 +- 13 files changed, 474 insertions(+), 69 deletions(-) create mode 100644 examples/preact/icon-192.png create mode 100644 examples/preact/icon-512.png create mode 100644 examples/preact/icon-maskable-512.png create mode 100644 examples/preact/manifest.webmanifest create mode 100644 examples/preact/sw.js diff --git a/README.md b/README.md index dc54223..3f081e6 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,9 @@ npm install npx serve . # serve.json sets the COOP/COEP headers the WASM module needs ``` -Open the printed URL in Chrome, plug the camera in over USB, switch it on and pick it from the device picker. +Open the printed URL in Chrome and plug the camera in. It connects on its own — there's no connect button. The one exception is the very first visit, which shows a "Choose camera" prompt: Chrome will not open its WebUSB device chooser without a click. After that the permission is remembered, and `navigator.usb.getDevices()` finds the camera with no interaction, so later visits go straight in. If the camera is off or asleep the page sits on "Looking for your camera…" and keeps retrying, connecting the moment it wakes up. + +It's also an installable PWA: install it from Chrome's address bar to get a standalone window, and it works with no network at all — the app shell and the pinned unpkg dependencies (including the ~2 MB WASM) are precached by a service worker. That matters here because the camera is on a USB cable, so the app is perfectly useful in a field with no signal. **Home screen** — only the things you change between runs: @@ -66,6 +68,7 @@ Open the printed URL in Chrome, plug the camera in over USB, switch it on and pi - Start/stop, plus a single-shot button for framing - Live countdown to the next frame, progress, estimated finish time, and a log of the last few events - A 🔊 toggle that speaks the countdown into each frame ("five, four, three, two, one") along with start and finish, for when you're in front of the camera rather than at the laptop +- A ⛶ button on the preview for a fullscreen live view, with the countdown carried over as an overlay so you don't lose it - A read-only strip showing shutter / aperture / ISO / format / battery / shots remaining, so you can sanity-check exposure against the interval without opening anything - Pre-flight warnings when the interval is too tight for the current exposure and transfer time, when you're shooting RAW on a short interval, or when no output folder is set diff --git a/examples/preact/build-dist.sh b/examples/preact/build-dist.sh index b13036e..1bbc590 100755 --- a/examples/preact/build-dist.sh +++ b/examples/preact/build-dist.sh @@ -12,16 +12,17 @@ set -eu cd "$(dirname "$0")" MODULES="index.js index-fallback.js home.js settings.js intervalometer.js - storage.js config-utils.js preview.js voice.js widget.js" + storage.js config-utils.js preview.js voice.js widget.js sw.js" +ASSETS="index.html _headers manifest.webmanifest + icon-192.png icon-512.png icon-maskable-512.png" # Collapse the line break to single spaces so the membership test below works. # shellcheck disable=SC2116,SC2086 MODULES=$(echo $MODULES) rm -rf dist mkdir -p dist -cp index.html _headers dist/ # shellcheck disable=SC2086 -cp $MODULES dist/ +cp $ASSETS $MODULES dist/ missing="" for f in *.js; do diff --git a/examples/preact/home.js b/examples/preact/home.js index 99b69df..bd5ddbf 100644 --- a/examples/preact/home.js +++ b/examples/preact/home.js @@ -98,45 +98,8 @@ function sequenceWarnings(prefs, config, hasFolder) { */ function SequenceStatus({ state, prefs, voiceSupported, onToggleVoice }) { let { phase, taken, total } = state; - let headline; - let detail; - - switch (phase) { - case 'delay': - headline = `Starting in ${formatDuration(secondsUntilNext(state))}`; - detail = 'Waiting out the start delay.'; - break; - case 'waiting': - headline = `Next frame in ${formatDuration(secondsUntilNext(state))}`; - detail = `${taken} of ${total || '∞'} captured`; - break; - case 'capturing': - headline = prefs.bulbEnabled - ? `⏱ Bulb exposure — ${prefs.bulbSeconds}s` - : '📸 Capturing…'; - detail = `${taken} of ${total || '∞'} captured`; - break; - case 'done': - headline = '✅ Sequence complete'; - detail = `${taken} frame${taken === 1 ? '' : 's'} in ${formatDuration( - (state.finishedAt - state.startedAt) / 1000 - )}`; - break; - case 'stopped': - headline = '⏹ Stopped'; - detail = `${taken} frame${taken === 1 ? '' : 's'} captured`; - break; - case 'failed': - headline = '❌ Sequence failed'; - detail = `${taken} captured, ${state.errors} failed`; - break; - default: - headline = 'Ready'; - detail = 'Set an interval and press Start.'; - } - + let { headline, detail } = sequenceSummary(state, prefs); let progress = total ? Math.min(1, taken / total) : 0; - return h( 'div', { class: `sequence-status phase-${phase}` }, @@ -178,6 +141,56 @@ function SequenceStatus({ state, prefs, voiceSupported, onToggleVoice }) { ); } +/** + * The one-line description of where the sequence is up to. Shared by the status + * card and the fullscreen preview overlay. + * + * @param {SequenceState} state + * @param {Prefs} prefs + * @returns {{ headline: string, detail: string }} + */ +export function sequenceSummary(state, prefs) { + let { phase, taken, total } = state; + let headline; + let detail; + + switch (phase) { + case 'delay': + headline = `Starting in ${formatDuration(secondsUntilNext(state))}`; + detail = 'Waiting out the start delay.'; + break; + case 'waiting': + headline = `Next frame in ${formatDuration(secondsUntilNext(state))}`; + detail = `${taken} of ${total || '∞'} captured`; + break; + case 'capturing': + headline = prefs.bulbEnabled + ? `⏱ Bulb exposure — ${prefs.bulbSeconds}s` + : '📸 Capturing…'; + detail = `${taken} of ${total || '∞'} captured`; + break; + case 'done': + headline = '✅ Sequence complete'; + detail = `${taken} frame${taken === 1 ? '' : 's'} in ${formatDuration( + (state.finishedAt - state.startedAt) / 1000 + )}`; + break; + case 'stopped': + headline = '⏹ Stopped'; + detail = `${taken} frame${taken === 1 ? '' : 's'} captured`; + break; + case 'failed': + headline = '❌ Sequence failed'; + detail = `${taken} captured, ${state.errors} failed`; + break; + default: + headline = 'Ready'; + detail = 'Set an interval and press Start.'; + } + + return { headline, detail }; +} + /** * The whole home-screen control column: interval timing, start/stop, progress, * and a running log. Camera settings deliberately live in the drawer instead. diff --git a/examples/preact/icon-192.png b/examples/preact/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..e50bfa1ab0364d87023e38c02de27a8cf69cd3be GIT binary patch literal 8710 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 literal 0 HcmV?d00001 diff --git a/examples/preact/icon-512.png b/examples/preact/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..af13e834e685d8a421154676d5207078d38db1d9 GIT binary patch literal 23297 zcmdqJWn7eB_cwaY3`57zozjgc9fKmEq@a|f2#9ny3=JwRp>ztONJvSCfOJTAHw@iy zF7Nwy|DXFj&v|p+oEL`|10UFXueJAHd&PIHMTCZ$A^|Q9E&u=okCo�RRgA2nDeJ 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 literal 0 HcmV?d00001 diff --git a/examples/preact/icon-maskable-512.png b/examples/preact/icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..c5d891fe3205a1322c83537bb45c798825666fb5 GIT binary patch literal 19898 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 DVCQa$ literal 0 HcmV?d00001 diff --git a/examples/preact/index.html b/examples/preact/index.html index 7a1c7aa..18a10dd 100644 --- a/examples/preact/index.html +++ b/examples/preact/index.html @@ -4,6 +4,10 @@ Intervalometer — Canon over WebUSB + + + + +
⌛ Loading…
diff --git a/examples/preact/index.js b/examples/preact/index.js index 328ad10..a5e709e 100644 --- a/examples/preact/index.js +++ b/examples/preact/index.js @@ -16,10 +16,10 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ -import { h, hydrate, Component, Fragment } from 'preact'; +import { h, hydrate, Component, Fragment, createRef } from 'preact'; import { Camera, rethrowIfCritical } from 'web-gphoto2'; import { Preview } from './preview.js'; -import { Home } from './home.js'; +import { Home, sequenceSummary } from './home.js'; import { SettingsDrawer, loadPrefs, savePrefs } from './settings.js'; import { Intervalometer, formatDuration } from './intervalometer.js'; import { FrameSaver, WakeLock } from './storage.js'; @@ -66,10 +66,16 @@ class App extends Component { #logId = 0; #configWatchToken = 0; #wasRunning = false; + viewerRef = createRef(); componentDidMount() { this.saver.mode = this.state.prefs.saveMode; this.syncNarrator(this.state.prefs); + // Held for as long as the app is open, not just while a sequence runs - + // you're usually mid-setup when the display would otherwise sleep. + if (this.state.prefs.keepAwake) this.wakeLock.acquire(); + + document.addEventListener('fullscreenchange', this.handleFullscreenChange); addEventListener('error', ({ message }) => this.log(`Uncaught error: ${message}`, 'error') @@ -92,9 +98,9 @@ class App extends Component { { once: true } ); - // Try to connect to camera at startup. - // If none is found among saved connections, it will fallback to a picker. - this.tryToConnectToCamera(); + // No connect button: just keep reaching for the camera from the moment + // the page loads. + this.autoConnect(); } /** @@ -128,7 +134,8 @@ class App extends Component { savePrefs(next); this.saver.mode = next.saveMode; this.syncNarrator(next); - if (!next.keepAwake) this.wakeLock.release(); + if (next.keepAwake) this.wakeLock.acquire(); + else this.wakeLock.release(); return { prefs: next }; }); }; @@ -152,23 +159,64 @@ class App extends Component { this.narrator.enabled = wasEnabled; }; - selectDevice = async () => { - // @ts-ignore - await Camera.showPicker(); - this.setState({ view: 'status', message: '⌛ Connecting…' }); - await this.tryToConnectToCamera(); + grantAccess = async () => { + try { + // @ts-ignore + await Camera.showPicker(); + } catch (err) { + // Dismissing the chooser is not an error worth shouting about. + return; + } + await this.connectOnce(); }; - async tryToConnectToCamera() { + /** + * Keep trying to attach to a camera the browser has already been given + * permission for, so landing on the page is all it takes. + * + * WebUSB will only show its device chooser from a user gesture, so the one + * case that genuinely can't be automated is the very first time - after that + * the permission is remembered and `getDevices()` sees the camera with no + * interaction at all. + */ + async autoConnect() { + // Reconnect the moment a known camera is plugged in or switched on. + // @ts-ignore + navigator.usb?.addEventListener?.('connect', () => this.connectOnce()); + + while (!this.camera) { + let permitted = await this.permittedDevices(); + if (permitted.length) { + this.setState({ view: 'searching' }); + if (await this.connectOnce()) return; + } else { + this.setState({ view: 'permission' }); + } + await wait(1500); + } + } + + async permittedDevices() { + try { + // @ts-ignore + return await navigator.usb.getDevices(); + } catch { + return []; + } + } + + /** @returns {Promise} whether we're now connected. */ + async connectOnce() { + if (this.camera) return true; /** @type {Camera} */ let camera; try { camera = new Camera(); await camera.connect(); - } catch (e) { - console.warn(e); - this.setState({ view: 'picker' }); - return; + } catch (err) { + // Expected while the camera is off, asleep, or claimed by another app. + console.debug('Camera not ready yet:', err); + return false; } this.camera = camera; let supportedOps = await camera.getSupportedOps(); @@ -179,6 +227,7 @@ class App extends Component { configValue(this.state.config, 'model') || 'camera'}.` ); + return true; } async refreshConfig() { @@ -295,12 +344,28 @@ class App extends Component { this.narrator.update(seq); this.setState({ seq }); if (this.#wasRunning && !running) { - this.wakeLock.release(); + // The wake lock stays - it's app-wide now, not sequence-scoped. this.refreshConfig(); } this.#wasRunning = running; } + handleFullscreenChange = () => { + this.setState({ fullscreen: !!document.fullscreenElement }); + }; + + toggleFullscreen = async () => { + try { + if (document.fullscreenElement) { + await document.exitFullscreen(); + } else { + await this.viewerRef.current?.requestFullscreen(); + } + } catch (err) { + this.log(`Could not toggle fullscreen: ${err}`, 'warn'); + } + }; + chooseFolder = async () => { try { let name = await this.saver.chooseFolder(); @@ -403,21 +468,38 @@ class App extends Component { render(/** @type {App['props']} */ props, /** @type {App['state']} */ state) { switch (state.view) { - case 'picker': + case 'searching': return h( 'div', { class: 'center' }, h('h1', null, '⏱ Intervalometer'), - h('p', null, 'Connect your Canon 450D over USB and switch it on.'), + h('p', { class: 'searching' }, h('span', { class: 'spinner' }), 'Looking for your camera…'), + h( + 'p', + { class: 'fine-print' }, + 'Switch the camera on and set the mode dial off Auto. It will connect on its own — no need to reload. On macOS, quit Photos and Image Capture if they grabbed it first.' + ) + ); + + case 'permission': + return h( + 'div', + { class: 'center' }, + h('h1', null, '⏱ Intervalometer'), + h( + 'p', + null, + 'One-off setup: the browser needs your permission to talk to the camera.' + ), h( 'button', - { type: 'button', class: 'primary big', onclick: this.selectDevice }, - '🔍 Select camera' + { type: 'button', class: 'primary big', onclick: this.grantAccess }, + '🔍 Choose camera' ), h( 'p', { class: 'fine-print' }, - 'Requires Chrome with WebUSB. On Linux you may need a udev rule, and on macOS you may need to quit Photos/Image Capture first. Built on ', + 'Only needed once — after this it connects by itself whenever you open the page. Chrome will not open its device chooser without a click, which is the one part of this that cannot be automatic. Built on ', h( 'a', { href: 'https://github.com/GoogleChromeLabs/web-gphoto2' }, @@ -440,7 +522,11 @@ class App extends Component { null, h( 'div', - { id: 'viewer' }, + { + id: 'viewer', + ref: this.viewerRef, + class: state.fullscreen ? 'is-fullscreen' : '' + }, showPreview ? h(Preview, { getPreview: () => this.camera.capturePreviewAsBlob(), @@ -461,7 +547,37 @@ class App extends Component { previewSupported ? 'Live view is turned off in Settings.' : 'This camera does not support live preview.' + ), + h( + 'button', + { + type: 'button', + class: 'viewer-button', + onclick: this.toggleFullscreen, + title: state.fullscreen + ? 'Exit fullscreen (Esc)' + : 'Fullscreen preview' + }, + state.fullscreen ? '✕' : '⛶' + ), + // Fullscreen hides the whole control column, so carry the + // countdown across rather than leaving you staring at a picture. + state.fullscreen + ? h( + 'div', + { class: 'viewer-status' }, + h( + 'span', + { class: 'viewer-status-headline' }, + sequenceSummary(state.seq, state.prefs).headline + ), + h( + 'span', + { class: 'viewer-status-detail' }, + sequenceSummary(state.seq, state.prefs).detail + ) ) + : undefined ), h(Home, { prefs: state.prefs, diff --git a/examples/preact/manifest.webmanifest b/examples/preact/manifest.webmanifest new file mode 100644 index 0000000..0f29bad --- /dev/null +++ b/examples/preact/manifest.webmanifest @@ -0,0 +1,22 @@ +{ + "name": "DSLR Intervalometer", + "short_name": "Intervalometer", + "description": "Shoot timelapses on a USB-connected DSLR from the browser.", + "start_url": "./", + "scope": "./", + "display": "standalone", + "orientation": "any", + "background_color": "#14161a", + "theme_color": "#14161a", + "categories": ["photo", "utilities"], + "icons": [ + { "src": "./icon-192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "./icon-512.png", "sizes": "512x512", "type": "image/png" }, + { + "src": "./icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/examples/preact/settings.js b/examples/preact/settings.js index b0d70ae..187f4e6 100644 --- a/examples/preact/settings.js +++ b/examples/preact/settings.js @@ -254,7 +254,7 @@ export class SettingsDrawer extends Component { Row, { label: 'Keep screen awake', - hint: 'Background tabs get their timers throttled, which ruins interval timing.' + hint: 'Held the whole time the app is open, not just during a sequence. Background tabs get their timers throttled, which ruins interval timing.' }, h('input', { type: 'checkbox', diff --git a/examples/preact/sw.js b/examples/preact/sw.js new file mode 100644 index 0000000..30e0a6d --- /dev/null +++ b/examples/preact/sw.js @@ -0,0 +1,147 @@ +/* + * Service worker: makes the app installable and usable with no network. + * + * Offline matters more here than for most web apps - the camera is on the end + * of a USB cable, so the app is fully functional in a field with no signal, as + * long as it can load at all. + * + * Two caches with deliberately different strategies: + * + * - Our own files: network-first. They change whenever the app is redeployed, + * and a stale module paired with fresh HTML is a miserable bug to chase. + * Cache is the fallback, which is what makes offline work. + * - The unpkg dependencies (preact, web-gphoto2, the ~2MB WASM): cache-first. + * Those URLs are pinned to exact versions and are immutable, so re-checking + * them over the network buys nothing. + */ + +const VERSION = 'v1'; +const SHELL_CACHE = `intervalometer-shell-${VERSION}`; +const DEPS_CACHE = `intervalometer-deps-${VERSION}`; +const CACHES = [SHELL_CACHE, DEPS_CACHE]; + +const SHELL = [ + './', + './index.html', + './manifest.webmanifest', + './index.js', + './index-fallback.js', + './home.js', + './settings.js', + './intervalometer.js', + './storage.js', + './config-utils.js', + './preview.js', + './voice.js', + './widget.js', + './icon-192.png', + './icon-512.png' +]; + +/** Cross-origin hosts whose responses are safe to keep indefinitely. */ +const IMMUTABLE_HOSTS = ['unpkg.com']; + +/* + * These have to be fetched at install time, not left to the runtime cache. + * On a first visit the worker isn't controlling the page yet, so the app's own + * imports go straight to the network and never reach the fetch handler - which + * means without this the app looks cached but dies offline on its imports. + * + * Every URL is version-pinned, so they're safe to hold forever. + */ +const DEPS = [ + 'https://unpkg.com/web-gphoto2@0.4.1/build/camera.js', + 'https://unpkg.com/web-gphoto2@0.4.1/build/libapi.mjs', + 'https://unpkg.com/web-gphoto2@0.4.1/build/libapi.wasm', + 'https://unpkg.com/preact@10.6.4/dist/preact.module.js', + 'https://unpkg.com/purecss@2.0.6/build/pure-min.css' +]; + +self.addEventListener('install', event => { + event.waitUntil( + Promise.all([ + caches.open(SHELL_CACHE).then(cache => cache.addAll(SHELL)), + precacheDeps() + ]).then(() => self.skipWaiting()) + ); +}); + +/** + * Fetched individually and tolerantly: a flaky CDN shouldn't fail the whole + * install and leave the app with no worker at all. Anything that misses here + * gets picked up by the runtime cache on a later online visit. + */ +async function precacheDeps() { + let cache = await caches.open(DEPS_CACHE); + await Promise.allSettled( + DEPS.map(async url => { + let response = await fetch(url, { mode: 'cors', credentials: 'omit' }); + if (!response.ok) throw new Error(`${response.status} for ${url}`); + await cache.put(url, response); + }) + ); +} + +self.addEventListener('activate', event => { + event.waitUntil( + caches + .keys() + .then(keys => + Promise.all(keys.filter(k => !CACHES.includes(k)).map(k => caches.delete(k))) + ) + .then(() => self.clients.claim()) + ); +}); + +self.addEventListener('fetch', event => { + let { request } = event; + if (request.method !== 'GET') return; + + let url = new URL(request.url); + + if (IMMUTABLE_HOSTS.includes(url.hostname)) { + event.respondWith(cacheFirst(request)); + return; + } + + if (url.origin === self.location.origin) { + event.respondWith(networkFirst(request)); + } +}); + +/** + * @param {Request} request + */ +async function cacheFirst(request) { + let cache = await caches.open(DEPS_CACHE); + let hit = await cache.match(request); + if (hit) return hit; + let response = await fetch(request); + // An opaque response would break the page's cross-origin isolation on + // replay, so only keep ones that actually passed CORS. + if (response.ok && response.type !== 'opaque') { + cache.put(request, response.clone()); + } + return response; +} + +/** + * @param {Request} request + */ +async function networkFirst(request) { + let cache = await caches.open(SHELL_CACHE); + try { + let response = await fetch(request); + if (response.ok) cache.put(request, response.clone()); + return response; + } catch (err) { + let hit = await cache.match(request); + if (hit) return hit; + // A navigation with nothing cached for this exact URL still wants the shell. + if (request.mode === 'navigate') { + let shell = await cache.match('./'); + if (shell) return shell; + } + throw err; + } +} diff --git a/examples/preact/tsconfig.json b/examples/preact/tsconfig.json index 6cd74fe..e8d1436 100644 --- a/examples/preact/tsconfig.json +++ b/examples/preact/tsconfig.json @@ -1,7 +1,9 @@ { // dist/ is a copy of these same files assembled for deploy; checking it too // just reports every error twice, against stale copies. - "exclude": ["dist", "node_modules"], + // sw.js runs in ServiceWorkerGlobalScope, not the DOM, so checking it in this + // program reports every worker global as undefined. + "exclude": ["dist", "node_modules", "sw.js"], "compilerOptions": { "checkJs": true, "target": "ESNext", diff --git a/examples/preact/types.d.ts b/examples/preact/types.d.ts index b71f80b..448fb8d 100644 --- a/examples/preact/types.d.ts +++ b/examples/preact/types.d.ts @@ -6,8 +6,12 @@ type LogEntry = { }; type AppState = { - /** Which top-level screen is showing. */ - view: 'status' | 'picker' | 'ready'; + /** + * Which top-level screen is showing. 'searching' auto-retries in the + * background; 'permission' is the one-off WebUSB grant, which the browser + * will not let us trigger without a click. + */ + view: 'status' | 'searching' | 'permission' | 'ready'; /** Shown while `view` is 'status'. */ message?: string; config?: import('web-gphoto2').Config; @@ -17,4 +21,6 @@ type AppState = { log: LogEntry[]; settingsOpen: boolean; singleShotStatus: 'idle' | 'busy'; + /** Whether the preview pane is currently filling the screen. */ + fullscreen?: boolean; };