@8bitbish/screenshot-service 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,701 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta http-equiv="Content-Security-Policy"
6
+ content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'" />
7
+ <title>Setup</title>
8
+ <style>
9
+ :root {
10
+ --bg: #0e0f12;
11
+ --panel: #16181d;
12
+ --panel-2: #1c1f26;
13
+ --border: #2a2e36;
14
+ --text: #e6e8ef;
15
+ --muted: #8b91a1;
16
+ --accent: #4f8bff;
17
+ --accent-hover: #6a9eff;
18
+ --ok: #3ddc97;
19
+ --warn: #f1c062;
20
+ --err: #ff6b6b;
21
+ }
22
+ * { box-sizing: border-box; }
23
+ html, body {
24
+ margin: 0;
25
+ height: 100%;
26
+ background: var(--bg);
27
+ color: var(--text);
28
+ font: 14px/1.5 -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", system-ui, sans-serif;
29
+ -webkit-font-smoothing: antialiased;
30
+ overflow: hidden;
31
+ user-select: none;
32
+ }
33
+ body {
34
+ display: flex;
35
+ flex-direction: column;
36
+ padding: 28px 32px 24px;
37
+ }
38
+ header {
39
+ display: flex;
40
+ align-items: baseline;
41
+ justify-content: space-between;
42
+ gap: 16px;
43
+ margin-bottom: 24px;
44
+ }
45
+ h1 {
46
+ font-size: 22px;
47
+ font-weight: 600;
48
+ margin: 0;
49
+ letter-spacing: -0.01em;
50
+ }
51
+ .step-pill {
52
+ color: var(--muted);
53
+ font-size: 12px;
54
+ text-transform: uppercase;
55
+ letter-spacing: 0.08em;
56
+ }
57
+ main {
58
+ flex: 1;
59
+ overflow-y: auto;
60
+ padding-right: 4px;
61
+ }
62
+ .screen { display: none; }
63
+ .screen.active { display: block; }
64
+ p { margin: 0 0 12px; color: var(--text); }
65
+ p.muted { color: var(--muted); }
66
+ .lede { font-size: 15px; margin-bottom: 20px; }
67
+ ol, ul { padding-left: 22px; margin: 12px 0; }
68
+ ol li, ul li { margin: 6px 0; }
69
+ kbd, code {
70
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
71
+ font-size: 12.5px;
72
+ background: var(--panel-2);
73
+ border: 1px solid var(--border);
74
+ border-radius: 4px;
75
+ padding: 1px 6px;
76
+ }
77
+ .live-dot {
78
+ display: inline-block;
79
+ width: 8px; height: 8px;
80
+ background: #28a745;
81
+ border-radius: 50%;
82
+ margin-right: 6px;
83
+ vertical-align: middle;
84
+ box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.7);
85
+ animation: live-pulse 1.5s ease-in-out infinite;
86
+ }
87
+ @keyframes live-pulse {
88
+ 0% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.6); }
89
+ 70% { box-shadow: 0 0 0 7px rgba(40, 167, 69, 0); }
90
+ 100% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0); }
91
+ }
92
+ .step-list { list-style: none; padding: 0; margin: 0; }
93
+ .step {
94
+ display: flex;
95
+ align-items: flex-start;
96
+ gap: 12px;
97
+ padding: 10px 14px;
98
+ background: var(--panel);
99
+ border: 1px solid var(--border);
100
+ border-radius: 10px;
101
+ margin-bottom: 8px;
102
+ }
103
+ .step .icon {
104
+ flex: 0 0 22px;
105
+ height: 22px;
106
+ border-radius: 999px;
107
+ display: inline-flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ font-size: 13px;
111
+ margin-top: 1px;
112
+ }
113
+ .step.pending .icon { background: var(--panel-2); color: var(--muted); }
114
+ .step.pending .icon::after { content: "◌"; }
115
+ .step.running .icon { background: rgba(79, 139, 255, 0.16); color: var(--accent); }
116
+ .step.running .icon::after { content: ""; width: 10px; height: 10px; border-radius: 999px; border: 2px solid currentColor; border-top-color: transparent; animation: spin 0.7s linear infinite; }
117
+ .step.done .icon { background: rgba(61, 220, 151, 0.16); color: var(--ok); }
118
+ .step.done .icon::after { content: "✓"; font-weight: 700; }
119
+ .step.skipped .icon { background: var(--panel-2); color: var(--muted); }
120
+ .step.skipped .icon::after { content: "✓"; }
121
+ .step.error .icon { background: rgba(255, 107, 107, 0.16); color: var(--err); }
122
+ .step.error .icon::after { content: "✗"; font-weight: 700; }
123
+ .step .body { flex: 1; min-width: 0; }
124
+ .step .label { font-weight: 500; }
125
+ .step .detail { font-size: 12.5px; color: var(--muted); margin-top: 2px; }
126
+ .log-box {
127
+ margin-top: 16px;
128
+ background: var(--panel);
129
+ border: 1px solid var(--border);
130
+ border-radius: 10px;
131
+ overflow: hidden;
132
+ }
133
+ .log-toggle {
134
+ width: 100%;
135
+ background: transparent;
136
+ border: 0;
137
+ text-align: left;
138
+ color: var(--muted);
139
+ font: inherit;
140
+ padding: 10px 14px;
141
+ cursor: pointer;
142
+ }
143
+ .log-toggle:hover { color: var(--text); }
144
+ .log-content {
145
+ display: none;
146
+ background: #0a0b0e;
147
+ border-top: 1px solid var(--border);
148
+ padding: 10px 14px;
149
+ max-height: 160px;
150
+ overflow-y: auto;
151
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
152
+ font-size: 12px;
153
+ color: var(--muted);
154
+ white-space: pre-wrap;
155
+ word-break: break-all;
156
+ }
157
+ .log-content.open { display: block; }
158
+ .field-grid {
159
+ display: grid;
160
+ grid-template-columns: 1fr 1fr 1fr;
161
+ gap: 12px;
162
+ margin: 16px 0;
163
+ }
164
+ .field label {
165
+ display: block;
166
+ font-size: 12px;
167
+ color: var(--muted);
168
+ margin-bottom: 6px;
169
+ text-transform: uppercase;
170
+ letter-spacing: 0.06em;
171
+ }
172
+ .field input {
173
+ width: 100%;
174
+ background: var(--panel);
175
+ border: 1px solid var(--border);
176
+ color: var(--text);
177
+ border-radius: 8px;
178
+ padding: 10px 12px;
179
+ font: inherit;
180
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
181
+ font-size: 14px;
182
+ outline: none;
183
+ transition: border-color 0.12s ease;
184
+ }
185
+ .field input:focus { border-color: var(--accent); }
186
+ .field input::placeholder { color: #4a5060; }
187
+ .choice-grid {
188
+ display: grid;
189
+ grid-template-columns: 1fr 1fr;
190
+ gap: 12px;
191
+ margin: 20px 0;
192
+ }
193
+ .choice {
194
+ text-align: left;
195
+ background: var(--panel);
196
+ border: 1px solid var(--border);
197
+ color: var(--text);
198
+ border-radius: 12px;
199
+ padding: 18px;
200
+ cursor: pointer;
201
+ font: inherit;
202
+ transition: border-color 0.12s ease, transform 0.12s ease;
203
+ }
204
+ .choice:hover { border-color: var(--accent); transform: translateY(-1px); }
205
+ .choice .h { font-weight: 600; margin-bottom: 4px; }
206
+ .choice .d { color: var(--muted); font-size: 13px; }
207
+ footer {
208
+ display: flex;
209
+ justify-content: space-between;
210
+ align-items: center;
211
+ gap: 12px;
212
+ margin-top: 20px;
213
+ padding-top: 16px;
214
+ border-top: 1px solid var(--border);
215
+ }
216
+ .btn {
217
+ background: var(--accent);
218
+ color: #fff;
219
+ border: 0;
220
+ border-radius: 8px;
221
+ padding: 10px 18px;
222
+ font: inherit;
223
+ font-weight: 500;
224
+ cursor: pointer;
225
+ transition: background 0.12s ease, opacity 0.12s ease;
226
+ }
227
+ .btn:hover { background: var(--accent-hover); }
228
+ .btn:disabled { opacity: 0.5; cursor: default; background: var(--accent); }
229
+ .btn.secondary {
230
+ background: transparent;
231
+ color: var(--muted);
232
+ border: 1px solid var(--border);
233
+ }
234
+ .btn.secondary:hover { color: var(--text); border-color: var(--text); }
235
+ .center { text-align: center; }
236
+ .big-check {
237
+ width: 72px;
238
+ height: 72px;
239
+ border-radius: 999px;
240
+ background: rgba(61, 220, 151, 0.16);
241
+ color: var(--ok);
242
+ margin: 40px auto 20px;
243
+ display: flex;
244
+ align-items: center;
245
+ justify-content: center;
246
+ font-size: 36px;
247
+ font-weight: 700;
248
+ }
249
+ .error-box {
250
+ background: rgba(255, 107, 107, 0.08);
251
+ border: 1px solid rgba(255, 107, 107, 0.4);
252
+ border-radius: 10px;
253
+ padding: 12px 14px;
254
+ color: var(--err);
255
+ font-size: 13px;
256
+ margin: 12px 0;
257
+ white-space: pre-wrap;
258
+ }
259
+ @keyframes spin { to { transform: rotate(360deg); } }
260
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
261
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
262
+ ::-webkit-scrollbar-track { background: transparent; }
263
+ </style>
264
+ </head>
265
+ <body>
266
+ <header>
267
+ <h1 id="title">Setup</h1>
268
+ <span class="step-pill" id="step-pill">Checking…</span>
269
+ </header>
270
+
271
+ <main>
272
+ <!-- Screen: detecting -->
273
+ <section class="screen active" id="screen-detect">
274
+ <p class="lede">Checking what your Mac needs to capture from your device.</p>
275
+ <ul class="step-list" id="detect-list">
276
+ <li class="step running"><span class="icon"></span><span class="body"><span class="label">Looking for required tools</span></span></li>
277
+ </ul>
278
+ </section>
279
+
280
+ <!-- Screen: install -->
281
+ <section class="screen" id="screen-install">
282
+ <p class="lede" id="install-lede">Installing what's missing. This takes a couple of minutes.</p>
283
+ <ul class="step-list" id="install-list"></ul>
284
+ <div class="log-box">
285
+ <button class="log-toggle" id="log-toggle">Show details</button>
286
+ <div class="log-content" id="log-content"></div>
287
+ </div>
288
+ <div class="error-box" id="install-error" style="display: none"></div>
289
+ </section>
290
+
291
+ <!-- Screen: iOS device trust -->
292
+ <section class="screen" id="screen-ios-device">
293
+ <p class="lede">Now pair your iPhone with this Mac. One-time setup.</p>
294
+ <ol>
295
+ <li>On iPhone: <strong>Settings → Privacy &amp; Security → Developer Mode</strong> → turn on → restart.</li>
296
+ <li>Plug iPhone into your Mac with a USB cable.</li>
297
+ <li>On iPhone, tap <strong>Trust</strong> when prompted; enter your passcode.</li>
298
+ <li>(Optional) In Finder, select your iPhone and tick <kbd>Show this iPhone when on Wi-Fi</kbd>, then unplug.</li>
299
+ </ol>
300
+ <p class="muted"><span class="live-dot"></span> Waiting for iPhone… we'll continue automatically once you tap Trust.</p>
301
+ </section>
302
+
303
+ <!-- Screen: Android connection-type chooser -->
304
+ <section class="screen" id="screen-android-choice">
305
+ <p class="lede">How do you want to connect your Android phone?</p>
306
+ <div class="choice-grid">
307
+ <button class="choice" data-conn="usb">
308
+ <div class="h">USB cable</div>
309
+ <div class="d">Plug in once and tap Allow. Easiest to get working.</div>
310
+ </button>
311
+ <button class="choice" data-conn="wireless">
312
+ <div class="h">Wireless</div>
313
+ <div class="d">Pair once, then capture without a cable. Needs Android 11+.</div>
314
+ </button>
315
+ </div>
316
+ </section>
317
+
318
+ <!-- Screen: Android USB instructions -->
319
+ <section class="screen" id="screen-android-usb">
320
+ <p class="lede">Enable USB debugging and plug in.</p>
321
+ <ol>
322
+ <li>On phone: <strong>Settings → About Phone</strong> → tap <strong>Build Number</strong> seven times until you see <em>"You are now a developer!"</em></li>
323
+ <li>Go to <strong>Settings → Developer Options</strong> → turn on <strong>USB Debugging</strong>.</li>
324
+ <li>Plug phone into Mac with a USB cable.</li>
325
+ <li>When the phone shows <em>"Allow USB debugging?"</em>, tap <strong>Allow</strong>.</li>
326
+ </ol>
327
+ <p class="muted"><span class="live-dot"></span> Waiting for your phone… we'll continue automatically once it's authorised.</p>
328
+ </section>
329
+
330
+ <!-- Screen: Android wireless pairing form -->
331
+ <section class="screen" id="screen-android-wireless">
332
+ <p class="lede">Pair wirelessly. Get the pairing code from your phone.</p>
333
+ <ol>
334
+ <li>On phone: <strong>Settings → Developer Options → Wireless Debugging</strong> → turn on.</li>
335
+ <li>Tap <strong>Pair device with pairing code</strong>.</li>
336
+ <li>Copy the IP, port, and 6-digit code into the fields below.</li>
337
+ </ol>
338
+ <div class="field-grid">
339
+ <div class="field">
340
+ <label for="pair-ip">IP address</label>
341
+ <input id="pair-ip" placeholder="192.168.1.50" inputmode="numeric" />
342
+ </div>
343
+ <div class="field">
344
+ <label for="pair-port">Pair port</label>
345
+ <input id="pair-port" placeholder="39847" inputmode="numeric" />
346
+ </div>
347
+ <div class="field">
348
+ <label for="pair-code">Code</label>
349
+ <input id="pair-code" placeholder="123456" inputmode="numeric" maxlength="6" />
350
+ </div>
351
+ </div>
352
+ <ul class="step-list" id="pair-list"></ul>
353
+ <div class="error-box" id="pair-error" style="display: none"></div>
354
+ </section>
355
+
356
+ <!-- Screen: done -->
357
+ <section class="screen" id="screen-done">
358
+ <div class="big-check">✓</div>
359
+ <p class="center" style="font-size: 17px; font-weight: 500">You're all set</p>
360
+ <p class="center muted" id="done-detail">Your device is connected and ready.</p>
361
+ </section>
362
+ </main>
363
+
364
+ <footer>
365
+ <button class="btn secondary" id="btn-back" style="visibility: hidden">Back</button>
366
+ <div style="flex: 1"></div>
367
+ <button class="btn" id="btn-primary">Continue</button>
368
+ </footer>
369
+
370
+ <script>
371
+ (() => {
372
+ const $ = (id) => document.getElementById(id);
373
+ const screens = {
374
+ detect: $('screen-detect'),
375
+ install: $('screen-install'),
376
+ iosDevice: $('screen-ios-device'),
377
+ androidChoice: $('screen-android-choice'),
378
+ androidUsb: $('screen-android-usb'),
379
+ androidWireless:$('screen-android-wireless'),
380
+ done: $('screen-done'),
381
+ };
382
+ const stepPill = $('step-pill');
383
+ const btnPrimary = $('btn-primary');
384
+ const btnBack = $('btn-back');
385
+
386
+ let platform = null; // 'ios' | 'android'
387
+ let lastIosStatus = null;
388
+ let lastAndroidStatus = null;
389
+ let history = [];
390
+
391
+ function show(name, label) {
392
+ for (const k in screens) screens[k].classList.remove('active');
393
+ screens[name].classList.add('active');
394
+ stepPill.textContent = label;
395
+ btnBack.style.visibility = history.length ? 'visible' : 'hidden';
396
+ }
397
+
398
+ function navigate(name, label) {
399
+ history.push({ name, label });
400
+ show(name, label);
401
+ }
402
+
403
+ btnBack.addEventListener('click', () => {
404
+ if (history.length <= 1) return;
405
+ history.pop();
406
+ const prev = history[history.length - 1];
407
+ show(prev.name, prev.label);
408
+ });
409
+
410
+ // ── Step list renderer ─────────────────────────────────────────────────────
411
+ function renderStep(listEl, p) {
412
+ let li = listEl.querySelector(`[data-id="${p.id}"]`);
413
+ if (!li) {
414
+ li = document.createElement('li');
415
+ li.className = 'step';
416
+ li.dataset.id = p.id;
417
+ li.innerHTML = '<span class="icon"></span><span class="body"><span class="label"></span><span class="detail"></span></span>';
418
+ listEl.appendChild(li);
419
+ }
420
+ li.className = `step ${p.status === 'running' ? 'running' : p.status === 'done' ? 'done' : p.status === 'skipped' ? 'skipped' : p.status === 'error' ? 'error' : 'pending'}`;
421
+ li.querySelector('.label').textContent = p.label;
422
+ const detail = li.querySelector('.detail');
423
+ if (p.detail) detail.textContent = p.detail; else detail.textContent = '';
424
+ }
425
+
426
+ // ── Log tail ────────────────────────────────────────────────────────────────
427
+ const logContent = $('log-content');
428
+ const logToggle = $('log-toggle');
429
+ logToggle.addEventListener('click', () => {
430
+ const open = logContent.classList.toggle('open');
431
+ logToggle.textContent = open ? 'Hide details' : 'Show details';
432
+ });
433
+ function appendLog(line) {
434
+ logContent.textContent += line + '\n';
435
+ logContent.scrollTop = logContent.scrollHeight;
436
+ }
437
+
438
+ // ── Live device polling ─────────────────────────────────────────────────────
439
+ // Polls every 1.5s while a "waiting for device" screen is active. Auto-advances
440
+ // to the done screen the moment the device shows up — no clicking required.
441
+ let livePollHandle = null;
442
+ function stopLivePoll() {
443
+ if (livePollHandle) { clearInterval(livePollHandle); livePollHandle = null; }
444
+ }
445
+ function startLivePoll(screenEl, check, onDetected) {
446
+ stopLivePoll();
447
+ livePollHandle = setInterval(async () => {
448
+ if (!screenEl.classList.contains('active')) { stopLivePoll(); return; }
449
+ try {
450
+ const status = await check();
451
+ if (onDetected(status)) {
452
+ stopLivePoll();
453
+ }
454
+ } catch {}
455
+ }, 1500);
456
+ }
457
+
458
+ // ── iOS flow ────────────────────────────────────────────────────────────────
459
+ async function runIosDetect() {
460
+ navigate('detect', 'Step 1 of 3');
461
+ btnPrimary.style.display = 'none';
462
+ const status = await window.wizard.checkIos();
463
+ lastIosStatus = status;
464
+ const list = $('detect-list');
465
+ list.innerHTML = '';
466
+ renderStep(list, { id: 'brew', label: 'Homebrew', status: status.brew ? 'done' : 'error' });
467
+ renderStep(list, { id: 'python', label: 'Python', status: status.pymobiledevice3 ? 'done' : 'error' });
468
+ renderStep(list, { id: 'pmd3', label: 'pymobiledevice3', status: status.pymobiledevice3 ? 'done' : 'error' });
469
+ renderStep(list, { id: 'sudoers', label: 'Passwordless tunnel', status: status.sudoers ? 'done' : 'pending', detail: status.sudoers ? '' : 'Optional — you can skip and enter your Mac password each session.' });
470
+ renderStep(list, { id: 'device', label: 'iPhone connected', status: status.deviceVisible ? 'done' : 'pending', detail: status.deviceVisible ? '' : 'Plug in your iPhone and tap Trust.' });
471
+
472
+ btnPrimary.style.display = '';
473
+ if (!status.ok) {
474
+ btnPrimary.textContent = 'Install missing tools';
475
+ btnPrimary.onclick = runIosInstall;
476
+ } else if (!status.deviceVisible) {
477
+ btnPrimary.textContent = 'Continue';
478
+ btnPrimary.onclick = () => navigate('iosDevice', 'Step 2 of 3');
479
+ } else {
480
+ btnPrimary.textContent = 'Finish';
481
+ btnPrimary.onclick = finish;
482
+ $('done-detail').textContent = 'Your iPhone is connected and ready.';
483
+ navigate('done', 'Done');
484
+ }
485
+ }
486
+
487
+ async function runIosInstall() {
488
+ navigate('install', 'Step 2 of 3');
489
+ $('install-list').innerHTML = '';
490
+ $('install-error').style.display = 'none';
491
+ btnPrimary.disabled = true;
492
+ btnPrimary.textContent = 'Installing…';
493
+
494
+ const off = window.wizard.onInstallIosProgress((p) => {
495
+ renderStep($('install-list'), p);
496
+ if (p.log) appendLog(p.log);
497
+ });
498
+
499
+ try {
500
+ await window.wizard.installIos();
501
+ btnPrimary.disabled = false;
502
+ btnPrimary.textContent = 'Continue';
503
+ btnPrimary.onclick = () => navigate('iosDevice', 'Step 3 of 3');
504
+ } catch (err) {
505
+ btnPrimary.disabled = false;
506
+ btnPrimary.textContent = 'Retry';
507
+ btnPrimary.onclick = runIosInstall;
508
+ $('install-error').textContent = err.message || String(err);
509
+ $('install-error').style.display = '';
510
+ } finally {
511
+ off();
512
+ }
513
+ }
514
+
515
+ // Re-check after iOS device step
516
+ async function recheckIos() {
517
+ const status = await window.wizard.checkIos();
518
+ if (status.deviceVisible) {
519
+ $('done-detail').textContent = 'Your iPhone is connected and ready.';
520
+ navigate('done', 'Done');
521
+ btnPrimary.textContent = 'Finish';
522
+ btnPrimary.onclick = finish;
523
+ } else {
524
+ $('install-error').textContent = "Still can't see your iPhone. Make sure it's plugged in, unlocked, and you tapped Trust.";
525
+ $('install-error').style.display = '';
526
+ }
527
+ }
528
+
529
+ // ── Android flow ───────────────────────────────────────────────────────────
530
+ async function runAndroidDetect() {
531
+ navigate('detect', 'Step 1 of 3');
532
+ btnPrimary.style.display = 'none';
533
+ const status = await window.wizard.checkAndroid();
534
+ lastAndroidStatus = status;
535
+ const list = $('detect-list');
536
+ list.innerHTML = '';
537
+ renderStep(list, { id: 'adb', label: 'ADB (Android Debug Bridge)', status: status.adb ? 'done' : 'error' });
538
+ renderStep(list, { id: 'device', label: 'Android device', status: status.deviceConnected ? 'done' : 'pending' });
539
+
540
+ btnPrimary.style.display = '';
541
+ if (!status.adb) {
542
+ btnPrimary.textContent = 'Install ADB';
543
+ btnPrimary.onclick = runAndroidInstall;
544
+ } else if (!status.deviceConnected) {
545
+ btnPrimary.textContent = 'Connect a device';
546
+ btnPrimary.onclick = () => navigate('androidChoice', 'Step 2 of 3');
547
+ } else {
548
+ $('done-detail').textContent = 'Your Android device is connected and ready.';
549
+ navigate('done', 'Done');
550
+ btnPrimary.textContent = 'Finish';
551
+ btnPrimary.onclick = finish;
552
+ }
553
+ }
554
+
555
+ async function runAndroidInstall() {
556
+ navigate('install', 'Step 2 of 3');
557
+ $('install-list').innerHTML = '';
558
+ $('install-error').style.display = 'none';
559
+ btnPrimary.disabled = true;
560
+ btnPrimary.textContent = 'Installing…';
561
+
562
+ const off = window.wizard.onInstallAndroidProgress((p) => {
563
+ renderStep($('install-list'), p);
564
+ if (p.log) appendLog(p.log);
565
+ });
566
+
567
+ try {
568
+ await window.wizard.installAndroid();
569
+ btnPrimary.disabled = false;
570
+ btnPrimary.textContent = 'Connect a device';
571
+ btnPrimary.onclick = () => navigate('androidChoice', 'Step 3 of 3');
572
+ } catch (err) {
573
+ btnPrimary.disabled = false;
574
+ btnPrimary.textContent = 'Retry';
575
+ btnPrimary.onclick = runAndroidInstall;
576
+ $('install-error').textContent = err.message || String(err);
577
+ $('install-error').style.display = '';
578
+ } finally {
579
+ off();
580
+ }
581
+ }
582
+
583
+ document.querySelectorAll('.choice').forEach(btn => {
584
+ btn.addEventListener('click', () => {
585
+ const conn = btn.dataset.conn;
586
+ if (conn === 'usb') {
587
+ navigate('androidUsb', 'Step 3 of 3');
588
+ btnPrimary.textContent = 'Check again';
589
+ btnPrimary.onclick = recheckAndroid;
590
+ // Auto-advance the moment ADB sees the phone — no clicking required.
591
+ startLivePoll(
592
+ screens.androidUsb,
593
+ () => window.wizard.checkAndroid(),
594
+ (status) => {
595
+ if (!status.deviceConnected) return false;
596
+ $('done-detail').textContent = 'Your Android device is connected and ready.';
597
+ navigate('done', 'Done');
598
+ btnPrimary.textContent = 'Finish';
599
+ btnPrimary.onclick = finish;
600
+ return true;
601
+ }
602
+ );
603
+ } else {
604
+ navigate('androidWireless', 'Step 3 of 3');
605
+ btnPrimary.textContent = 'Pair device';
606
+ btnPrimary.onclick = runAndroidPair;
607
+ }
608
+ });
609
+ });
610
+
611
+ async function recheckAndroid() {
612
+ const status = await window.wizard.checkAndroid();
613
+ if (status.deviceConnected) {
614
+ $('done-detail').textContent = 'Your Android device is connected and ready.';
615
+ navigate('done', 'Done');
616
+ btnPrimary.textContent = 'Finish';
617
+ btnPrimary.onclick = finish;
618
+ } else {
619
+ $('install-error').textContent = "No device detected yet. Check the cable, allow USB debugging on the phone, and try again.";
620
+ $('install-error').style.display = '';
621
+ }
622
+ }
623
+
624
+ async function runAndroidPair() {
625
+ const ip = $('pair-ip').value.trim();
626
+ const port = $('pair-port').value.trim();
627
+ const code = $('pair-code').value.trim();
628
+ $('pair-error').style.display = 'none';
629
+ if (!/^\d+\.\d+\.\d+\.\d+$/.test(ip)) { $('pair-error').textContent = 'IP address looks wrong (expected e.g. 192.168.1.50).'; $('pair-error').style.display = ''; return; }
630
+ if (!/^\d+$/.test(port)) { $('pair-error').textContent = 'Pair port should be a number.'; $('pair-error').style.display = ''; return; }
631
+ if (!/^\d{6}$/.test(code)) { $('pair-error').textContent = 'Pairing code must be 6 digits.'; $('pair-error').style.display = ''; return; }
632
+
633
+ btnPrimary.disabled = true;
634
+ btnPrimary.textContent = 'Pairing…';
635
+ $('pair-list').innerHTML = '';
636
+
637
+ const off = window.wizard.onPairAndroidProgress((p) => {
638
+ renderStep($('pair-list'), p);
639
+ });
640
+
641
+ try {
642
+ await window.wizard.pairAndroid({ ip, port, code });
643
+ btnPrimary.disabled = false;
644
+ $('done-detail').textContent = 'Your Android device is paired and ready.';
645
+ navigate('done', 'Done');
646
+ btnPrimary.textContent = 'Finish';
647
+ btnPrimary.onclick = finish;
648
+ } catch (err) {
649
+ btnPrimary.disabled = false;
650
+ btnPrimary.textContent = 'Try again';
651
+ btnPrimary.onclick = runAndroidPair;
652
+ $('pair-error').textContent = err.message || String(err);
653
+ $('pair-error').style.display = '';
654
+ } finally {
655
+ off();
656
+ }
657
+ }
658
+
659
+ function finish() {
660
+ window.wizard.close();
661
+ }
662
+
663
+ // ── Boot ────────────────────────────────────────────────────────────────────
664
+ window.wizard.init().then(({ platform: p }) => {
665
+ platform = p;
666
+ $('title').textContent = p === 'ios' ? 'Set up iPhone capture' : 'Set up Android capture';
667
+
668
+ // When user clicks "Check again" on a device screen, route to the right recheck.
669
+ btnPrimary.addEventListener('click', () => {/* per-screen handlers attach onclick directly */});
670
+
671
+ if (p === 'ios') {
672
+ runIosDetect();
673
+ // When iosDevice screen activates: keep "Check again" as a manual fallback
674
+ // AND start live polling so the wizard auto-advances the moment trust is granted.
675
+ const obs = new MutationObserver(() => {
676
+ if (screens.iosDevice.classList.contains('active')) {
677
+ btnPrimary.textContent = 'Check again';
678
+ btnPrimary.onclick = recheckIos;
679
+ startLivePoll(
680
+ screens.iosDevice,
681
+ () => window.wizard.checkIos(),
682
+ (status) => {
683
+ if (!status.deviceVisible) return false;
684
+ $('done-detail').textContent = 'Your iPhone is connected and ready.';
685
+ navigate('done', 'Done');
686
+ btnPrimary.textContent = 'Finish';
687
+ btnPrimary.onclick = finish;
688
+ return true;
689
+ }
690
+ );
691
+ }
692
+ });
693
+ obs.observe(screens.iosDevice, { attributes: true, attributeFilter: ['class'] });
694
+ } else {
695
+ runAndroidDetect();
696
+ }
697
+ });
698
+ })();
699
+ </script>
700
+ </body>
701
+ </html>