phone-lens 1.1.0 → 1.1.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.
package/lib/client.js CHANGED
@@ -268,6 +268,7 @@ window.__ModuleLoader__.load({
268
268
  const [rename, setRename] = useState(null); // {id, name} — self-drawn rename dialog
269
269
  const [appQr, setAppQr] = useState(null); // app-download QR dialog payload (or {loading}/{fallback:true})
270
270
  const [previewOff, setPreviewOff] = useState(false); // host says the phone's stream stopped (preview off on the phone)
271
+ const [previewOffline, setPreviewOffline] = useState(false); // host flagged the LINK itself dead (wifi drop, no frames AND no keepalive) — not a preview toggle
271
272
  const [settingsOpen, setSettingsOpen] = useState(false); // collapsible settings section under the main card
272
273
  const [appSt, setAppSt] = useState(null); // {saveMode, saveDir, effectiveSaveDir} | null (null = host too old)
273
274
  const [dirDraft, setDirDraft] = useState("");
@@ -276,6 +277,11 @@ window.__ModuleLoader__.load({
276
277
  const wsRef = useRef(null);
277
278
  const framesRef = useRef(0);
278
279
  const rotRef = useRef(0);
280
+ // last devices-broadcast length — the onmessage closure never re-runs
281
+ // (the WS effect depends only on wsUrl), so comparing against the
282
+ // `devices` STATE there read a stale first-render value and fired the
283
+ // QR-rotation flash on EVERY broadcast instead of only on growth
284
+ const lastDeviceCount = useRef(0);
279
285
 
280
286
  const baseUrl = `http://127.0.0.1:${port}`;
281
287
  const wsUrl = `ws://127.0.0.1:${port}/ws/view`;
@@ -341,9 +347,13 @@ window.__ModuleLoader__.load({
341
347
  } else if (m.type === "frame_meta") {
342
348
  if (m.rotation !== undefined) rotRef.current = m.rotation;
343
349
  } else if (m.type === "preview_state") {
344
- // host-derived: the ACTIVE phone's stream stopped (user turned
345
- // preview off in the app) or started again
350
+ // host-derived: the ACTIVE phone's stream stopped — either the
351
+ // user turned preview off in the app (keepalive still alive) or
352
+ // the link itself died (no frames AND no keepalive for 25s →
353
+ // reason:"offline"). Old hosts never send `reason` → legacy
354
+ // behavior is kept bit-for-bit.
346
355
  setPreviewOff(!m.on);
356
+ setPreviewOffline(!m.on && m.reason === "offline");
347
357
  } else if (m.type === "upload_saved") {
348
358
  // folder-only mode: the photo bypassed the composer; local hint
349
359
  setFlashOk(`已保存至 ${m.dir || "指定文件夹"}`);
@@ -363,8 +373,10 @@ window.__ModuleLoader__.load({
363
373
  .catch((e) => setFlashErr("自动放入失败: " + String(e && e.message || e).slice(0, 90)));
364
374
  } else if (m.type === "devices") {
365
375
  // side effects OUT of the updater (updaters must stay pure —
366
- // StrictMode double-invokes them); compare here, setState below
367
- const grew = (m.devices || []).length > (devices ? devices.length : 0);
376
+ // StrictMode double-invokes them); compare against the ref,
377
+ // NOT the `devices` state (stale-closure trap), then setState
378
+ const grew = (m.devices || []).length > lastDeviceCount.current;
379
+ lastDeviceCount.current = (m.devices || []).length;
368
380
  setDevices(m.devices || []);
369
381
  // a NEW device came online → the one-time pairing code was
370
382
  // consumed; rotate the QR so the next scan sees a fresh code
@@ -380,6 +392,12 @@ window.__ModuleLoader__.load({
380
392
  if (prev === "disconnected") return "connecting";
381
393
  return prev;
382
394
  });
395
+ } else if (m.type === "paired") {
396
+ // a phone completed /pair — the one-time pairing code burned
397
+ // with it. Rotate the QR right now; the devices-list growth
398
+ // path also rotates, but only once the uplink actually lands.
399
+ setFlashOk(`设备已配对${m.name ? ": " + m.name : ""},二维码已更换`);
400
+ void refreshQr(true);
383
401
  } else if (m.type === "error") {
384
402
  setFlashErr(`${m.code}${m.message ? ":" + m.message : ""}`);
385
403
  }
@@ -387,6 +405,7 @@ window.__ModuleLoader__.load({
387
405
  ws.onclose = () => {
388
406
  setCamOn(false);
389
407
  setPreviewOff(false);
408
+ setPreviewOffline(false);
390
409
  if (!stopped) timer = setTimeout(connect, 2000);
391
410
  };
392
411
  };
@@ -577,11 +596,13 @@ window.__ModuleLoader__.load({
577
596
  { className: "lm-body" },
578
597
  // only show the viewfinder canvas while a camera is actually
579
598
  // streaming; when the phone turned preview off, clear the last
580
- // frame and say so instead of freezing on a stale picture
599
+ // frame and say so instead of freezing on a stale picture.
600
+ // previewOffline = the host proved the LINK died (no frames AND
601
+ // no keepalive 25s): say "connection lost", never "preview off"
581
602
  camOn && !previewOff
582
603
  ? h("canvas", { ref: canvasRef, className: "lm-canvas", width: 360, height: 640 })
583
604
  : camOn && previewOff
584
- ? h("div", { className: "lm-preview-off" }, "您在手机端已关闭预览功能")
605
+ ? h("div", { className: "lm-preview-off" }, previewOffline ? "与手机的连接已中断(设备离线)" : "您在手机端已关闭预览功能")
585
606
  : null,
586
607
  h(
587
608
  "div",
@@ -589,12 +610,14 @@ window.__ModuleLoader__.load({
589
610
  h("span", null, camOn && !previewOff && fps > 0 ? `${fps} fps` : "—"),
590
611
  h(
591
612
  "span",
592
- { className: !camOn ? "warn-big" : previewOff ? "warn" : "" },
613
+ { className: !camOn || previewOffline ? "warn-big" : previewOff ? "warn" : "" },
593
614
  !camOn
594
615
  ? "手机未连接!相同局域网下扫码添加配对设备;若已配对过的手机,请在设置内点击本电脑端,激活为活动设备"
595
- : previewOff
596
- ? "预览已关闭,在手机端重新开启后恢复"
597
- : "预览中",
616
+ : previewOffline
617
+ ? "连接已断开(手机离线),请检查手机网络;网络恢复后将自动重连"
618
+ : previewOff
619
+ ? "预览已关闭,在手机端重新开启后恢复"
620
+ : "预览中",
598
621
  ),
599
622
  ),
600
623
  // ── version hints (plain colored lines, never dialogs) ──
@@ -918,9 +941,11 @@ window.__ModuleLoader__.load({
918
941
  { className: "lm-setsec-rows" },
919
942
  // tool names in the open (user-requested): what the
920
943
  // model will actually see in its tool list
921
- infoRow("拍摄照片(phone_take_photo)", "手机立即拍照,照片直接进入模型视野——不放入输入框、不保存到电脑文件夹,与拍照注入模式设置无关。", "w1"),
922
- infoRow("关闭手机摄像头(phone_camera_pause)", "让手机相机提前进入空闲省电状态。", "w2"),
923
- infoRow("恢复手机摄像头(phone_camera_resume)", "立即打开相机并恢复预览拍摄。", "w3"),
944
+ infoRow("拍摄照片(phone_take_photo)", "手机立即拍照,照片直接进入模型视野——不放入输入框、不保存到电脑文件夹,与拍照注入模式设置无关。", "w1"),
945
+ infoRow("关闭手机摄像头(phone_camera_pause)", "让手机相机提前进入空闲省电状态。", "w2"),
946
+ infoRow("恢复手机摄像头(phone_camera_resume)", "立即打开相机并恢复预览拍摄。", "w3"),
947
+ infoRow("调整变焦(phone_camera_zoom)", "按指定倍数(或步进)拉近/拉远手机镜头,取景画面与照片同步生效,超出设备范围自动收拢。", "w4"),
948
+ infoRow("查询变焦(phone_camera_zoom_info)", "读取手机当前变焦倍数与设备支持的变焦范围。", "w5"),
924
949
  ),
925
950
  ),
926
951
  flash ? h("div", { className: "lm-setsec-note", key: "f" }, flash) : null,