Make capture sessions continuous: click-capture + interval auto-capture
Template tests / tests (push) Failing after 29s
Template tests / tests (push) Failing after 29s
The session previously only listened for the global hotkey, which is unreliable under WSLg/Wayland — users got one screenshot and nothing more. Sessions now layer three triggers: - click-capture via OS adapters (xinput test-xi2 on X11, PowerShell GetAsyncKeyState polling on Windows), debounced, ignoring clicks on StepForge itself - interval auto-capture (3/5/10 s) as the always-works fallback, enabled by default when click detection is unavailable - the existing global hotkey, plus a manual Shoot button The REC bar now shows live count + active trigger with Shoot / Auto / Pause / Finish. New captures and added steps are selected in the editor (explicit reload(stepId) wins over a surviving selection). Capture self-test hook (STEPFORGE_CAPTURE_SELFTEST) verifies 3x hotkey-path captures and interval capture end-to-end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,13 @@ Initial release.
|
|||||||
- Capture menu in the editor topbar: full screen / window / region /
|
- Capture menu in the editor topbar: full screen / window / region /
|
||||||
3-second delay, paste image as step, import images, and capture
|
3-second delay, paste image as step, import images, and capture
|
||||||
session start/finish — capture no longer requires the global hotkey.
|
session start/finish — capture no longer requires the global hotkey.
|
||||||
|
- Continuous capture sessions: steps are grabbed on every OS click where
|
||||||
|
the platform supports it (xinput on X11, PowerShell on Windows), with
|
||||||
|
interval auto-capture (3/5/10 s) as the always-works fallback when
|
||||||
|
click detection or global hotkeys are unavailable (e.g. WSLg/Wayland).
|
||||||
|
The REC bar shows the live count and trigger, with Shoot / Auto /
|
||||||
|
Pause / Finish controls.
|
||||||
|
- New captures and newly added steps are now selected in the editor.
|
||||||
- The app hides its own window during capture so screenshots show your
|
- The app hides its own window during capture so screenshots show your
|
||||||
work, not StepForge; hotkey captures restore the window without
|
work, not StepForge; hotkey captures restore the window without
|
||||||
stealing focus.
|
stealing focus.
|
||||||
|
|||||||
@@ -48,10 +48,13 @@ The core workflow:
|
|||||||
- **Guide library** with folders, favorites, title search, full-text search,
|
- **Guide library** with folders, favorites, title search, full-text search,
|
||||||
duplicate/move/delete, and a quick-actions palette (`Ctrl+/`).
|
duplicate/move/delete, and a quick-actions palette (`Ctrl+/`).
|
||||||
- **Capture engine** — the editor's **Capture ▾** button offers full screen,
|
- **Capture engine** — the editor's **Capture ▾** button offers full screen,
|
||||||
active window, and region capture (the app hides itself during the shot)
|
active window, and region capture (the app hides itself during the shot),
|
||||||
plus delay, pause/resume sessions with global hotkeys, click markers,
|
plus continuous capture sessions that grab a step on every click where the
|
||||||
clipboard paste, and PNG/JPEG/GIF import. The full keyboard shortcut list
|
OS allows it, or on a 3/5/10 s auto-interval everywhere else (the REC bar
|
||||||
lives under **More ▾ → Keyboard shortcuts** in the editor.
|
shows the live count with Shoot/Auto/Pause/Finish controls). Delay, global
|
||||||
|
hotkeys, click markers, clipboard paste, and PNG/JPEG/GIF import included.
|
||||||
|
The full keyboard shortcut list lives under **More ▾ → Keyboard
|
||||||
|
shortcuts** in the editor.
|
||||||
- **Three-pane editor** — step tree with substeps, statuses
|
- **Three-pane editor** — step tree with substeps, statuses
|
||||||
(todo/in-progress/done), hidden/skipped steps, focused view (zoom/pan that
|
(todo/in-progress/done), hidden/skipped steps, focused view (zoom/pan that
|
||||||
never mutates the original image), autosave, and command-stack undo/redo.
|
never mutates the original image), autosave, and command-stack undo/redo.
|
||||||
|
|||||||
+154
-9
@@ -1,49 +1,127 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
|
const { spawn, execFileSync } = require('node:child_process');
|
||||||
const { desktopCapturer, screen, BrowserWindow, nativeImage } = require('electron');
|
const { desktopCapturer, screen, BrowserWindow, nativeImage } = require('electron');
|
||||||
const { expandPlaceholders } = require('../core/placeholders');
|
const { expandPlaceholders } = require('../core/placeholders');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capture service: full-screen, active-window, and region capture via
|
* Capture service: full-screen, active-window, and region capture via
|
||||||
* Electron's desktopCapturer, plus a click-marker annotation at the cursor
|
* Electron's desktopCapturer, plus a click-marker annotation at the cursor
|
||||||
* position and a capture session (start/pause/resume/finish) driven by the
|
* position and a capture session (start/pause/resume/finish).
|
||||||
* global hotkey.
|
*
|
||||||
|
* A session captures continuously, with three triggers layered by what the
|
||||||
|
* platform supports:
|
||||||
|
* - click-capture via an OS adapter (xinput on X11, PowerShell on Windows),
|
||||||
|
* - a global hotkey (unreliable on some Wayland compositors),
|
||||||
|
* - interval auto-capture as the always-works fallback.
|
||||||
*
|
*
|
||||||
* Note: under Wayland/WSLg, screen capture may require portal support; all
|
* Note: under Wayland/WSLg, screen capture may require portal support; all
|
||||||
* failures surface as { ok: false, reason } instead of crashing.
|
* failures surface as { ok: false, reason } instead of crashing.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const CLICK_DEBOUNCE_MS = 700;
|
||||||
|
|
||||||
|
function hasBinary(name) {
|
||||||
|
try {
|
||||||
|
execFileSync('which', [name], { stdio: 'pipe' });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class CaptureService {
|
class CaptureService {
|
||||||
constructor({ store, settings, getWindow, notify }) {
|
constructor({ store, settings, getWindow, notify }) {
|
||||||
this.store = store;
|
this.store = store;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.getWindow = getWindow;
|
this.getWindow = getWindow;
|
||||||
this.notify = notify;
|
this.notify = notify;
|
||||||
this.session = null; // { guideId, paused, count }
|
this.session = null; // { guideId, paused, count, intervalSec }
|
||||||
|
this.intervalTimer = null;
|
||||||
|
this.clickWatcher = null;
|
||||||
|
this.lastClickCapture = 0;
|
||||||
|
this.shooting = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
state() {
|
state() {
|
||||||
return this.session
|
return this.session
|
||||||
? { active: true, paused: this.session.paused, guideId: this.session.guideId, count: this.session.count }
|
? {
|
||||||
: { active: false };
|
active: true,
|
||||||
|
paused: this.session.paused,
|
||||||
|
guideId: this.session.guideId,
|
||||||
|
count: this.session.count,
|
||||||
|
intervalSec: this.session.intervalSec || 0,
|
||||||
|
clickCapture: Boolean(this.clickWatcher),
|
||||||
|
clickCaptureAvailable: this.clickCaptureAvailable(),
|
||||||
|
}
|
||||||
|
: { active: false, clickCaptureAvailable: this.clickCaptureAvailable() };
|
||||||
}
|
}
|
||||||
|
|
||||||
startSession(guideId) {
|
clickCaptureAvailable() {
|
||||||
this.session = { guideId, paused: false, count: 0 };
|
if (this._clickAvail === undefined) {
|
||||||
|
this._clickAvail = process.platform === 'win32' || (process.platform === 'linux' && hasBinary('xinput'));
|
||||||
|
}
|
||||||
|
return this._clickAvail;
|
||||||
|
}
|
||||||
|
|
||||||
|
startSession(guideId, { intervalSec = null } = {}) {
|
||||||
|
this.finishSession();
|
||||||
|
// Default trigger: clicks when the platform supports it, otherwise an
|
||||||
|
// interval so a session always produces steps even if the global hotkey
|
||||||
|
// never fires (common under Wayland/WSLg).
|
||||||
|
let interval = intervalSec;
|
||||||
|
if (interval == null) {
|
||||||
|
interval = this.clickCaptureAvailable() ? 0 : (this.settings.get('capture.autoIntervalSec') || 5);
|
||||||
|
}
|
||||||
|
this.session = { guideId, paused: false, count: 0, intervalSec: interval };
|
||||||
|
if (this.settings.get('capture.captureOutsideClicks') !== false) this.startClickWatcher();
|
||||||
|
this.applyInterval();
|
||||||
|
this.notify('capture:state', this.state());
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(intervalSec) {
|
||||||
|
if (!this.session) return this.state();
|
||||||
|
this.session.intervalSec = Math.max(0, Number(intervalSec) || 0);
|
||||||
|
this.applyInterval();
|
||||||
|
this.notify('capture:state', this.state());
|
||||||
|
return this.state();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyInterval() {
|
||||||
|
if (this.intervalTimer) {
|
||||||
|
clearInterval(this.intervalTimer);
|
||||||
|
this.intervalTimer = null;
|
||||||
|
}
|
||||||
|
const sec = this.session && this.session.intervalSec;
|
||||||
|
if (sec > 0) {
|
||||||
|
this.intervalTimer = setInterval(() => {
|
||||||
|
this.sessionCapture('interval').catch(() => {});
|
||||||
|
}, sec * 1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
togglePause(force) {
|
togglePause(force) {
|
||||||
if (!this.session) return;
|
if (!this.session) return;
|
||||||
this.session.paused = typeof force === 'boolean' ? force : !this.session.paused;
|
this.session.paused = typeof force === 'boolean' ? force : !this.session.paused;
|
||||||
|
this.notify('capture:state', this.state());
|
||||||
}
|
}
|
||||||
|
|
||||||
finishSession() {
|
finishSession() {
|
||||||
|
if (this.intervalTimer) {
|
||||||
|
clearInterval(this.intervalTimer);
|
||||||
|
this.intervalTimer = null;
|
||||||
|
}
|
||||||
|
this.stopClickWatcher();
|
||||||
this.session = null;
|
this.session = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async hotkeyCapture() {
|
/** One capture inside the active session (hotkey/click/interval/manual). */
|
||||||
|
async sessionCapture(trigger = 'hotkey') {
|
||||||
if (!this.session || this.session.paused) return { ok: false, reason: 'no active capture session' };
|
if (!this.session || this.session.paused) return { ok: false, reason: 'no active capture session' };
|
||||||
|
if (this.shooting) return { ok: false, reason: 'capture already in progress' };
|
||||||
|
this.shooting = true;
|
||||||
|
try {
|
||||||
const mode = this.settings.get('capture.mode') || 'fullscreen';
|
const mode = this.settings.get('capture.mode') || 'fullscreen';
|
||||||
const result = await this.shoot({
|
const result = await this.shoot({
|
||||||
guideId: this.session.guideId,
|
guideId: this.session.guideId,
|
||||||
@@ -53,9 +131,76 @@ class CaptureService {
|
|||||||
});
|
});
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
this.session.count += 1;
|
this.session.count += 1;
|
||||||
this.notify('capture:added', { guideId: this.session.guideId, step: result.step });
|
this.notify('capture:added', { guideId: this.session.guideId, step: result.step, trigger });
|
||||||
|
this.notify('capture:state', this.state());
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
} finally {
|
||||||
|
this.shooting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hotkeyCapture() {
|
||||||
|
return this.sessionCapture('hotkey');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- click-triggered capture --------------------------------------------
|
||||||
|
|
||||||
|
startClickWatcher() {
|
||||||
|
this.stopClickWatcher();
|
||||||
|
try {
|
||||||
|
if (process.platform === 'linux' && hasBinary('xinput')) {
|
||||||
|
// Stream raw button events from the X server; one capture per press.
|
||||||
|
this.clickWatcher = spawn('xinput', ['test-xi2', '--root'], { stdio: ['ignore', 'pipe', 'ignore'] });
|
||||||
|
let sawPress = false;
|
||||||
|
this.clickWatcher.stdout.on('data', (chunk) => {
|
||||||
|
const text = chunk.toString();
|
||||||
|
if (/RawButtonPress|ButtonPress/.test(text)) sawPress = true;
|
||||||
|
if (sawPress) {
|
||||||
|
sawPress = false;
|
||||||
|
this.onOsClick();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (process.platform === 'win32') {
|
||||||
|
// Poll the left mouse button via GetAsyncKeyState; print one line per click.
|
||||||
|
const ps = `
|
||||||
|
Add-Type -Namespace W -Name U -MemberDefinition '[DllImport("user32.dll")] public static extern short GetAsyncKeyState(int k);'
|
||||||
|
$down = $false
|
||||||
|
while ($true) {
|
||||||
|
$s = [W.U]::GetAsyncKeyState(0x01) -band 0x8000
|
||||||
|
if ($s -and -not $down) { Write-Output CLICK }
|
||||||
|
$down = [bool]$s
|
||||||
|
Start-Sleep -Milliseconds 40
|
||||||
|
}`;
|
||||||
|
this.clickWatcher = spawn('powershell.exe', ['-NoProfile', '-Command', ps], { stdio: ['ignore', 'pipe', 'ignore'] });
|
||||||
|
this.clickWatcher.stdout.on('data', (chunk) => {
|
||||||
|
if (chunk.toString().includes('CLICK')) this.onOsClick();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.clickWatcher) {
|
||||||
|
this.clickWatcher.on('error', () => { this.clickWatcher = null; });
|
||||||
|
this.clickWatcher.on('exit', () => { this.clickWatcher = null; });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
this.clickWatcher = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopClickWatcher() {
|
||||||
|
if (this.clickWatcher) {
|
||||||
|
try { this.clickWatcher.kill(); } catch { /* already gone */ }
|
||||||
|
this.clickWatcher = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOsClick() {
|
||||||
|
if (!this.session || this.session.paused) return;
|
||||||
|
// Ignore clicks on StepForge itself (pausing, finishing, editing).
|
||||||
|
if (BrowserWindow.getFocusedWindow()) return;
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - this.lastClickCapture < CLICK_DEBOUNCE_MS) return;
|
||||||
|
this.lastClickCapture = now;
|
||||||
|
this.sessionCapture('click').catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
autoTitle(mode) {
|
autoTitle(mode) {
|
||||||
|
|||||||
+33
-2
@@ -98,6 +98,35 @@ function createWindow() {
|
|||||||
}
|
}
|
||||||
}, 1500);
|
}, 1500);
|
||||||
}
|
}
|
||||||
|
// Dev-only self-test: exercise the exact hotkey-session capture path
|
||||||
|
// (hide window -> grab -> showInactive) several times, then exit.
|
||||||
|
if (process.env.STEPFORGE_CAPTURE_SELFTEST) {
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const guide = store.createGuide({ title: 'hotkey selftest' });
|
||||||
|
capture.startSession(guide.guideId, { intervalSec: 0 });
|
||||||
|
const results = [];
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const r = await capture.hotkeyCapture();
|
||||||
|
results.push(r.ok ? 'OK' : `FAIL:${r.reason}`);
|
||||||
|
await new Promise((res) => setTimeout(res, 500));
|
||||||
|
}
|
||||||
|
console.log('HOTKEY-SELFTEST', JSON.stringify(results),
|
||||||
|
'steps:', store.getGuide(guide.guideId).stepsOrder.length);
|
||||||
|
|
||||||
|
// Interval auto-capture: 1s timer should add ~3 steps in 3.6s.
|
||||||
|
const guide2 = store.createGuide({ title: 'interval selftest' });
|
||||||
|
capture.startSession(guide2.guideId, { intervalSec: 1 });
|
||||||
|
await new Promise((res) => setTimeout(res, 3600));
|
||||||
|
capture.finishSession();
|
||||||
|
console.log('INTERVAL-SELFTEST steps:', store.getGuide(guide2.guideId).stepsOrder.length);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('SELFTEST ERROR', err.message);
|
||||||
|
} finally {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
mainWindow.on('closed', () => { mainWindow = null; });
|
mainWindow.on('closed', () => { mainWindow = null; });
|
||||||
}
|
}
|
||||||
@@ -267,11 +296,13 @@ function setupIpc() {
|
|||||||
if (result.ok) reindex(guideId);
|
if (result.ok) reindex(guideId);
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
h('capture:session', ({ action, guideId }) => {
|
h('capture:session', async ({ action, guideId, intervalSec }) => {
|
||||||
if (action === 'start') capture.startSession(guideId);
|
if (action === 'start') capture.startSession(guideId, { intervalSec: intervalSec ?? null });
|
||||||
else if (action === 'pause') capture.togglePause(true);
|
else if (action === 'pause') capture.togglePause(true);
|
||||||
else if (action === 'resume') capture.togglePause(false);
|
else if (action === 'resume') capture.togglePause(false);
|
||||||
else if (action === 'finish') capture.finishSession();
|
else if (action === 'finish') capture.finishSession();
|
||||||
|
else if (action === 'interval') capture.setInterval(intervalSec);
|
||||||
|
else if (action === 'shoot') await capture.sessionCapture('manual');
|
||||||
const state = capture.state();
|
const state = capture.state();
|
||||||
sendToRenderer('capture:state', state);
|
sendToRenderer('capture:state', state);
|
||||||
return state;
|
return state;
|
||||||
|
|||||||
+39
-10
@@ -81,6 +81,7 @@ class StepForgeApp {
|
|||||||
|
|
||||||
async onCaptureAdded(payload) {
|
async onCaptureAdded(payload) {
|
||||||
if (!payload || !payload.guideId) return;
|
if (!payload || !payload.guideId) return;
|
||||||
|
this.updateCaptureState(await api.capture.state());
|
||||||
if (this.state.view === 'editor' && this.editor.guideId === payload.guideId) {
|
if (this.state.view === 'editor' && this.editor.guideId === payload.guideId) {
|
||||||
await this.editor.reload(payload.step && payload.step.stepId ? payload.step.stepId : this.editor.selectedStepId);
|
await this.editor.reload(payload.step && payload.step.stepId ? payload.step.stepId : this.editor.selectedStepId);
|
||||||
return;
|
return;
|
||||||
@@ -187,7 +188,13 @@ class StepForgeApp {
|
|||||||
const state = await api.capture.session({ action: 'start', guideId: guide.guideId });
|
const state = await api.capture.session({ action: 'start', guideId: guide.guideId });
|
||||||
this.updateCaptureState(state);
|
this.updateCaptureState(state);
|
||||||
const hotkey = this.state.settings?.capture?.hotkeyCapture;
|
const hotkey = this.state.settings?.capture?.hotkeyCapture;
|
||||||
toast(hotkey ? `Capture session started — press ${hotkey} to grab a step.` : 'Capture session started.');
|
if (state.clickCapture) {
|
||||||
|
toast('Capture session started — every click outside StepForge grabs a step.');
|
||||||
|
} else if (state.intervalSec > 0) {
|
||||||
|
toast(`Capture session started — auto-capturing every ${state.intervalSec}s (use the REC bar to pause or change).`);
|
||||||
|
} else {
|
||||||
|
toast(hotkey ? `Capture session started — press ${hotkey} or use Shoot in the REC bar.` : 'Capture session started.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async openExistingWorkspace() {
|
async openExistingWorkspace() {
|
||||||
@@ -229,21 +236,43 @@ class StepForgeApp {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.captureStatus.classList.remove('hidden');
|
this.captureStatus.classList.remove('hidden');
|
||||||
|
const s = this.captureState;
|
||||||
|
const send = (payload) => api.capture.session(payload).then((next) => this.updateCaptureState(next));
|
||||||
|
|
||||||
|
// What is currently triggering captures, so the user knows what to do.
|
||||||
|
const trigger = s.paused ? 'paused'
|
||||||
|
: s.clickCapture ? 'on click'
|
||||||
|
: s.intervalSec > 0 ? `every ${s.intervalSec}s`
|
||||||
|
: 'hotkey only';
|
||||||
|
|
||||||
|
const shootBtn = el('button', {
|
||||||
|
type: 'button',
|
||||||
|
title: 'Capture a step now (the app hides itself for the shot)',
|
||||||
|
onClick: () => send({ action: 'shoot' }),
|
||||||
|
}, 'Shoot');
|
||||||
|
|
||||||
|
// Cycle interval auto-capture: off -> 3s -> 5s -> 10s -> off.
|
||||||
|
const nextInterval = { 0: 3, 3: 5, 5: 10, 10: 0 }[s.intervalSec ?? 0] ?? 3;
|
||||||
|
const autoBtn = el('button', {
|
||||||
|
type: 'button',
|
||||||
|
title: 'Automatically capture a step on a timer',
|
||||||
|
onClick: () => send({ action: 'interval', intervalSec: nextInterval }),
|
||||||
|
}, s.intervalSec > 0 ? `Auto ${s.intervalSec}s` : 'Auto off');
|
||||||
|
|
||||||
const pauseBtn = el('button', {
|
const pauseBtn = el('button', {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
onClick: () => {
|
onClick: () => send({ action: s.paused ? 'resume' : 'pause' }),
|
||||||
const action = this.captureState.paused ? 'resume' : 'pause';
|
}, s.paused ? 'Resume' : 'Pause');
|
||||||
api.capture.session({ action, guideId: this.editorMeta?.guide?.id || this.editorMeta?.guide?.guideId || null })
|
|
||||||
.then((next) => this.updateCaptureState(next));
|
|
||||||
},
|
|
||||||
}, this.captureState.paused ? 'Resume' : 'Pause');
|
|
||||||
const finishBtn = el('button', {
|
const finishBtn = el('button', {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
onClick: () => api.capture.session({ action: 'finish', guideId: this.editorMeta?.guide?.id || this.editorMeta?.guide?.guideId || null })
|
onClick: () => send({ action: 'finish' }),
|
||||||
.then((next) => this.updateCaptureState(next)),
|
|
||||||
}, 'Finish');
|
}, 'Finish');
|
||||||
|
|
||||||
this.captureStatus.append(
|
this.captureStatus.append(
|
||||||
el('span', {}, `Capture ${this.captureState.count || 0}`),
|
el('span', { title: `Capture session — ${trigger}` }, `REC ${s.count || 0} · ${trigger}`),
|
||||||
|
shootBtn,
|
||||||
|
autoBtn,
|
||||||
pauseBtn,
|
pauseBtn,
|
||||||
finishBtn,
|
finishBtn,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -125,8 +125,12 @@ class GuideEditor {
|
|||||||
this.steps = steps;
|
this.steps = steps;
|
||||||
this.stepMap = new Map(steps.map((step) => [step.stepId, step]));
|
this.stepMap = new Map(steps.map((step) => [step.stepId, step]));
|
||||||
if (!this.shellMounted) this.mountShell();
|
if (!this.shellMounted) this.mountShell();
|
||||||
if (!this.selectedStepId || !this.stepMap.has(this.selectedStepId)) {
|
// An explicitly requested step (new capture, added step, restored
|
||||||
this.selectedStepId = stepId && this.stepMap.has(stepId) ? stepId : (steps[0] && steps[0].stepId) || null;
|
// neighbour) wins; otherwise keep the current selection if it survived.
|
||||||
|
if (stepId && this.stepMap.has(stepId)) {
|
||||||
|
this.selectedStepId = stepId;
|
||||||
|
} else if (!this.selectedStepId || !this.stepMap.has(this.selectedStepId)) {
|
||||||
|
this.selectedStepId = (steps[0] && steps[0].stepId) || null;
|
||||||
}
|
}
|
||||||
this.selectedAnnotationId = null;
|
this.selectedAnnotationId = null;
|
||||||
this.renderAll();
|
this.renderAll();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
hotkeyPauseResume: 'CommandOrControl+Shift+2',
|
hotkeyPauseResume: 'CommandOrControl+Shift+2',
|
||||||
captureOutsideClicks: true,
|
captureOutsideClicks: true,
|
||||||
confirmSimpleCapture: false,
|
confirmSimpleCapture: false,
|
||||||
|
autoIntervalSec: 5, // session fallback when click capture is unavailable
|
||||||
},
|
},
|
||||||
editor: {
|
editor: {
|
||||||
focusedViewDefaultForNewSteps: false,
|
focusedViewDefaultForNewSteps: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user