phone-lens 1.0.3 → 1.1.1
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 +318 -17
- package/lib/dev.js +429 -47
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +553 -52
- package/lib/index.js.map +1 -1
- package/package.json +3 -1
package/lib/client.js
CHANGED
|
@@ -26,8 +26,8 @@ window.__ModuleLoader__.load({
|
|
|
26
26
|
{ id: "folder", title: "仅保存到文件夹", desc: "不放入输入框,只保存到指定电脑文件夹", flash: "已切换:仅保存到文件夹" },
|
|
27
27
|
];
|
|
28
28
|
// App download fallbacks (used when /app-qr.json is unavailable on an older host).
|
|
29
|
-
// Gitee release carrying the newest APK
|
|
30
|
-
const APP_GITEE = "https://gitee.com/qianfengbingtang/phone-lens/releases/download/
|
|
29
|
+
// Gitee release carrying the newest APK.
|
|
30
|
+
const APP_GITEE = "https://gitee.com/qianfengbingtang/phone-lens/releases/download/v1.1.0/app-release.apk";
|
|
31
31
|
const APP_GITHUB = "https://github.com/yxqfg/phone-lens/releases/latest/download/app-release.apk";
|
|
32
32
|
const h = react.createElement;
|
|
33
33
|
|
|
@@ -104,6 +104,31 @@ window.__ModuleLoader__.load({
|
|
|
104
104
|
.lm-fab.connecting { color: #ffffff; background: #6cc0ff; animation: lm-blast .5s ease-in-out 3; }
|
|
105
105
|
.lm-fab.disconnected { background: #23303c; color: #8fa1b5; }
|
|
106
106
|
.lm-status .warn-big { color: #d9a441; font-weight: 600; font-size: 11px; line-height: 1.4; white-space: normal; }
|
|
107
|
+
.lm-updwarn { font-size: 11px; line-height: 1.5; color: #d9a441; }
|
|
108
|
+
.lm-appver { font-size: 10px; line-height: 1.5; color: #7d8da0; }
|
|
109
|
+
.lm-appver.mismatch { color: #d9a441; }
|
|
110
|
+
.lm-setsec { display: flex; flex-direction: column; gap: 12px; max-width: 760px; }
|
|
111
|
+
.lm-setsec-head { display: flex; align-items: center; gap: 10px; }
|
|
112
|
+
.lm-setsec-headtitle { font-size: 16px; font-weight: 600; color: var(--dsw-alias-label-primary, #e6edf5); }
|
|
113
|
+
.lm-setsec-badge { font-size: 11px; line-height: 18px; padding: 0 10px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.14)); color: var(--dsw-alias-label-secondary, #9fb0c0); }
|
|
114
|
+
.lm-setsec-intro { font-size: 12px; line-height: 20px; color: var(--dsw-alias-label-tertiary, #7d8da0); }
|
|
115
|
+
.lm-setsec-card { border: 1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.10)); border-radius: 16px; background: var(--dsw-alias-bg-layer-3, rgba(255,255,255,.03)); overflow: hidden; }
|
|
116
|
+
.lm-setsec-cardhead { padding: 12px 16px 10px; font-size: 14px; font-weight: 600; color: var(--dsw-alias-label-primary, #e6edf5); }
|
|
117
|
+
.lm-setsec-rows { display: flex; flex-direction: column; }
|
|
118
|
+
.lm-set-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 16px; border-top: 1px solid var(--dsw-alias-border-l1, rgba(255,255,255,.06)); }
|
|
119
|
+
.lm-set-rowtext { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
|
120
|
+
.lm-set-rowtitle { font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-primary, #e6edf5); }
|
|
121
|
+
.lm-set-rowdesc { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary, #7d8da0); }
|
|
122
|
+
.lm-set-control { flex: none; display: inline-flex; align-items: center; }
|
|
123
|
+
.lm-set-switch { position: relative; display: inline-flex; flex: none; cursor: pointer; }
|
|
124
|
+
.lm-set-switch input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
|
|
125
|
+
.lm-set-track { display: inline-flex; align-items: center; width: 36px; height: 20px; padding: 2px; box-sizing: border-box; border-radius: 10px; border: 1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.14)); background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,.05)); transition: background .15s ease, border-color .15s ease; }
|
|
126
|
+
.lm-set-thumb { display: block; width: 14px; height: 14px; border-radius: 50%; background: var(--dsw-alias-label-tertiary, #8fa1b5); transition: transform .15s ease, background .15s ease; }
|
|
127
|
+
.lm-set-switch:hover .lm-set-track { border-color: var(--dsw-alias-label-dimmed, #5c6b7a); }
|
|
128
|
+
.lm-set-switch input:checked + .lm-set-track { border-color: var(--dsw-alias-button-primary-fill, #3b82f6); background: var(--dsw-alias-button-primary-fill, #3b82f6); }
|
|
129
|
+
.lm-set-switch input:checked + .lm-set-track .lm-set-thumb { transform: translateX(16px); background: var(--dsw-alias-bg-layer-3, #0f141a); }
|
|
130
|
+
.lm-set-switch input:focus-visible + .lm-set-track { outline: 2px solid var(--dsw-alias-button-primary-fill, #3b82f6); outline-offset: 2px; }
|
|
131
|
+
.lm-setsec-note { font-size: 12px; line-height: 20px; color: var(--dsw-alias-label-tertiary, #7d8da0); }
|
|
107
132
|
`;
|
|
108
133
|
// The factory runs at materialization (first import), so the document
|
|
109
134
|
// head is guaranteed to exist; remove-then-add keeps HMR idempotent.
|
|
@@ -235,10 +260,15 @@ window.__ModuleLoader__.load({
|
|
|
235
260
|
const [, setTick] = useState(0); // 1s heartbeat: re-renders the QR countdown while pairing is visible
|
|
236
261
|
const [flash, setFlash] = useState("");
|
|
237
262
|
const [pending, setPending] = useState([]); // [{id, name}] waiting to be staged
|
|
238
|
-
const [devices, setDevices] = useState([]); // [{id,name,active}]
|
|
263
|
+
const [devices, setDevices] = useState([]); // [{id,name,active,appVersion?}]
|
|
264
|
+
// Host plugin-update check (update-check.ts): null = unchecked/failed —
|
|
265
|
+
// the hint line simply doesn't render. Fetched once per panel-open; the
|
|
266
|
+
// host caches the Gitee lookup for 12h so this costs no API call.
|
|
267
|
+
const [upd, setUpd] = useState(null);
|
|
239
268
|
const [rename, setRename] = useState(null); // {id, name} — self-drawn rename dialog
|
|
240
269
|
const [appQr, setAppQr] = useState(null); // app-download QR dialog payload (or {loading}/{fallback:true})
|
|
241
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
|
|
242
272
|
const [settingsOpen, setSettingsOpen] = useState(false); // collapsible settings section under the main card
|
|
243
273
|
const [appSt, setAppSt] = useState(null); // {saveMode, saveDir, effectiveSaveDir} | null (null = host too old)
|
|
244
274
|
const [dirDraft, setDirDraft] = useState("");
|
|
@@ -251,6 +281,26 @@ window.__ModuleLoader__.load({
|
|
|
251
281
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
252
282
|
const wsUrl = `ws://127.0.0.1:${port}/ws/view`;
|
|
253
283
|
|
|
284
|
+
// plugin-update check, once per panel-open (host caches the Gitee
|
|
285
|
+
// lookup 12h / failures 30min — this fetch never hits the API itself)
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
if (!open) return;
|
|
288
|
+
let alive = true;
|
|
289
|
+
void (async () => {
|
|
290
|
+
try {
|
|
291
|
+
const r = await fetch(`${baseUrl}/update-check`, { cache: "no-store" });
|
|
292
|
+
if (!r.ok) throw new Error(String(r.status));
|
|
293
|
+
const data = await r.json();
|
|
294
|
+
if (alive) setUpd(data && data.current ? data : null);
|
|
295
|
+
} catch {
|
|
296
|
+
if (alive) setUpd(null);
|
|
297
|
+
}
|
|
298
|
+
})();
|
|
299
|
+
return () => {
|
|
300
|
+
alive = false;
|
|
301
|
+
};
|
|
302
|
+
}, [open, port]);
|
|
303
|
+
|
|
254
304
|
// view websocket: ALWAYS connected (independent of panel open state),
|
|
255
305
|
// so the connection status and pending-photo events refresh even when
|
|
256
306
|
// the FAB is collapsed. Frames are only drawn when the canvas exists.
|
|
@@ -292,9 +342,13 @@ window.__ModuleLoader__.load({
|
|
|
292
342
|
} else if (m.type === "frame_meta") {
|
|
293
343
|
if (m.rotation !== undefined) rotRef.current = m.rotation;
|
|
294
344
|
} else if (m.type === "preview_state") {
|
|
295
|
-
// host-derived: the ACTIVE phone's stream stopped
|
|
296
|
-
// preview off in the app) or
|
|
345
|
+
// host-derived: the ACTIVE phone's stream stopped — either the
|
|
346
|
+
// user turned preview off in the app (keepalive still alive) or
|
|
347
|
+
// the link itself died (no frames AND no keepalive for 25s →
|
|
348
|
+
// reason:"offline"). Old hosts never send `reason` → legacy
|
|
349
|
+
// behavior is kept bit-for-bit.
|
|
297
350
|
setPreviewOff(!m.on);
|
|
351
|
+
setPreviewOffline(!m.on && m.reason === "offline");
|
|
298
352
|
} else if (m.type === "upload_saved") {
|
|
299
353
|
// folder-only mode: the photo bypassed the composer; local hint
|
|
300
354
|
setFlashOk(`已保存至 ${m.dir || "指定文件夹"}`);
|
|
@@ -313,12 +367,13 @@ window.__ModuleLoader__.load({
|
|
|
313
367
|
})
|
|
314
368
|
.catch((e) => setFlashErr("自动放入失败: " + String(e && e.message || e).slice(0, 90)));
|
|
315
369
|
} else if (m.type === "devices") {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
370
|
+
// side effects OUT of the updater (updaters must stay pure —
|
|
371
|
+
// StrictMode double-invokes them); compare here, setState below
|
|
372
|
+
const grew = (m.devices || []).length > (devices ? devices.length : 0);
|
|
373
|
+
setDevices(m.devices || []);
|
|
374
|
+
// a NEW device came online → the one-time pairing code was
|
|
375
|
+
// consumed; rotate the QR so the next scan sees a fresh code
|
|
376
|
+
if (grew) void refreshQr(true);
|
|
322
377
|
// an active device present == camera linked; derive both the
|
|
323
378
|
// canvas visibility and the header/FAB link state from the
|
|
324
379
|
// device list (host no longer emits `device`)
|
|
@@ -337,6 +392,7 @@ window.__ModuleLoader__.load({
|
|
|
337
392
|
ws.onclose = () => {
|
|
338
393
|
setCamOn(false);
|
|
339
394
|
setPreviewOff(false);
|
|
395
|
+
setPreviewOffline(false);
|
|
340
396
|
if (!stopped) timer = setTimeout(connect, 2000);
|
|
341
397
|
};
|
|
342
398
|
};
|
|
@@ -527,11 +583,13 @@ window.__ModuleLoader__.load({
|
|
|
527
583
|
{ className: "lm-body" },
|
|
528
584
|
// only show the viewfinder canvas while a camera is actually
|
|
529
585
|
// streaming; when the phone turned preview off, clear the last
|
|
530
|
-
// frame and say so instead of freezing on a stale picture
|
|
586
|
+
// frame and say so instead of freezing on a stale picture.
|
|
587
|
+
// previewOffline = the host proved the LINK died (no frames AND
|
|
588
|
+
// no keepalive 25s): say "connection lost", never "preview off"
|
|
531
589
|
camOn && !previewOff
|
|
532
590
|
? h("canvas", { ref: canvasRef, className: "lm-canvas", width: 360, height: 640 })
|
|
533
591
|
: camOn && previewOff
|
|
534
|
-
? h("div", { className: "lm-preview-off" }, "您在手机端已关闭预览功能")
|
|
592
|
+
? h("div", { className: "lm-preview-off" }, previewOffline ? "与手机的连接已中断(设备离线)" : "您在手机端已关闭预览功能")
|
|
535
593
|
: null,
|
|
536
594
|
h(
|
|
537
595
|
"div",
|
|
@@ -539,14 +597,31 @@ window.__ModuleLoader__.load({
|
|
|
539
597
|
h("span", null, camOn && !previewOff && fps > 0 ? `${fps} fps` : "—"),
|
|
540
598
|
h(
|
|
541
599
|
"span",
|
|
542
|
-
{ className: !camOn ? "warn-big" : previewOff ? "warn" : "" },
|
|
600
|
+
{ className: !camOn || previewOffline ? "warn-big" : previewOff ? "warn" : "" },
|
|
543
601
|
!camOn
|
|
544
602
|
? "手机未连接!相同局域网下扫码添加配对设备;若已配对过的手机,请在设置内点击本电脑端,激活为活动设备"
|
|
545
|
-
:
|
|
546
|
-
? "
|
|
547
|
-
:
|
|
603
|
+
: previewOffline
|
|
604
|
+
? "连接已断开(手机离线),请检查手机网络;网络恢复后将自动重连"
|
|
605
|
+
: previewOff
|
|
606
|
+
? "预览已关闭,在手机端重新开启后恢复"
|
|
607
|
+
: "预览中",
|
|
548
608
|
),
|
|
549
609
|
),
|
|
610
|
+
// ── version hints (plain colored lines, never dialogs) ──
|
|
611
|
+
upd && upd.updateAvailable
|
|
612
|
+
? h("div", { className: "lm-updwarn", key: "upd" }, `⬆ 电脑端有新版本 v${upd.latest} — 请在 设置 → 插件市场 更新`)
|
|
613
|
+
: null,
|
|
614
|
+
(() => {
|
|
615
|
+
// cross-end consistency: the phone reports its App version
|
|
616
|
+
// in hello; a mismatch (both ends known) warms the line
|
|
617
|
+
const app = (devices || []).find((d) => d.active)?.appVersion;
|
|
618
|
+
if (!app) return null;
|
|
619
|
+
const host = upd && upd.current;
|
|
620
|
+
if (host && app !== host) {
|
|
621
|
+
return h("div", { className: "lm-appver mismatch", key: "appver" }, `手机 App v${app} 与电脑端 v${host} 版本不一致,建议两端同时更新`);
|
|
622
|
+
}
|
|
623
|
+
return h("div", { className: "lm-appver", key: "appver" }, `手机 App v${app}`);
|
|
624
|
+
})(),
|
|
550
625
|
devices && devices.length > 1
|
|
551
626
|
? h(
|
|
552
627
|
"div",
|
|
@@ -711,6 +786,223 @@ window.__ModuleLoader__.load({
|
|
|
711
786
|
);
|
|
712
787
|
}
|
|
713
788
|
|
|
789
|
+
// ── settings.section — the official settings-page panel ───────────────
|
|
790
|
+
// Privacy-bearing switches live here with an honest description of what
|
|
791
|
+
// the model can do and where photos go. The high-frequency capture-mode
|
|
792
|
+
// settings stay in the overlay ⚙ panel on purpose (switched constantly).
|
|
793
|
+
function currentLensPort() {
|
|
794
|
+
try {
|
|
795
|
+
return window.localStorage.getItem(PORT_KEY) || "8791";
|
|
796
|
+
} catch {
|
|
797
|
+
return "8791";
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Custom switch per the DSH settings-row recipe: a real checkbox (native
|
|
802
|
+
// semantics + focus) driving a styled track/thumb — 36×20 track, 14px
|
|
803
|
+
// thumb, primary fill when checked. Sizes/tokens mirror dsh-better-sidebar.
|
|
804
|
+
function LensSwitch({ checked, onChange }) {
|
|
805
|
+
return h(
|
|
806
|
+
"label",
|
|
807
|
+
{ className: "lm-set-switch" },
|
|
808
|
+
h("input", {
|
|
809
|
+
type: "checkbox",
|
|
810
|
+
checked: !!checked,
|
|
811
|
+
onChange: (e) => onChange(e.target.checked),
|
|
812
|
+
}),
|
|
813
|
+
h("span", { className: "lm-set-track", "aria-hidden": "true" }, h("span", { className: "lm-set-thumb" })),
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// One settings row: title/desc on the left, control on the right.
|
|
818
|
+
function LensSetRow({ title, desc, children }) {
|
|
819
|
+
return h(
|
|
820
|
+
"div",
|
|
821
|
+
{ className: "lm-set-row" },
|
|
822
|
+
h(
|
|
823
|
+
"span",
|
|
824
|
+
{ className: "lm-set-rowtext" },
|
|
825
|
+
h("span", { className: "lm-set-rowtitle" }, title),
|
|
826
|
+
desc ? h("span", { className: "lm-set-rowdesc" }, desc) : null,
|
|
827
|
+
),
|
|
828
|
+
h("span", { className: "lm-set-control" }, children),
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function LensSettingsSection() {
|
|
833
|
+
const [st, setSt] = useState(undefined); // undefined=loading, false=unavailable, object=live
|
|
834
|
+
const [flash, setFlash] = useState("");
|
|
835
|
+
|
|
836
|
+
useEffect(() => {
|
|
837
|
+
let alive = true;
|
|
838
|
+
void (async () => {
|
|
839
|
+
try {
|
|
840
|
+
const r = await fetch(`http://127.0.0.1:${currentLensPort()}/app-settings`, { cache: "no-store" });
|
|
841
|
+
if (!r.ok) throw new Error(String(r.status));
|
|
842
|
+
const data = await r.json();
|
|
843
|
+
if (alive) setSt(data);
|
|
844
|
+
} catch {
|
|
845
|
+
if (alive) setSt(false);
|
|
846
|
+
}
|
|
847
|
+
})();
|
|
848
|
+
return () => {
|
|
849
|
+
alive = false;
|
|
850
|
+
};
|
|
851
|
+
}, []);
|
|
852
|
+
|
|
853
|
+
async function patch(p) {
|
|
854
|
+
try {
|
|
855
|
+
const r = await fetch(`http://127.0.0.1:${currentLensPort()}/app-settings`, {
|
|
856
|
+
method: "POST",
|
|
857
|
+
headers: { "content-type": "application/json" },
|
|
858
|
+
body: JSON.stringify(p),
|
|
859
|
+
});
|
|
860
|
+
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
861
|
+
setSt(await r.json());
|
|
862
|
+
setFlash("已保存");
|
|
863
|
+
setTimeout(() => setFlash(""), 1800);
|
|
864
|
+
} catch (e) {
|
|
865
|
+
setFlash("保存失败: " + String((e && e.message) || e).slice(0, 60));
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const infoRow = (title, desc, key) =>
|
|
870
|
+
h(
|
|
871
|
+
"div",
|
|
872
|
+
{ className: "lm-set-row", key },
|
|
873
|
+
h(
|
|
874
|
+
"span",
|
|
875
|
+
{ className: "lm-set-rowtext" },
|
|
876
|
+
h("span", { className: "lm-set-rowtitle" }, title),
|
|
877
|
+
h("span", { className: "lm-set-rowdesc" }, desc),
|
|
878
|
+
),
|
|
879
|
+
);
|
|
880
|
+
|
|
881
|
+
return h(
|
|
882
|
+
"div",
|
|
883
|
+
{ className: "lm-setsec" },
|
|
884
|
+
h(
|
|
885
|
+
"div",
|
|
886
|
+
{ className: "lm-setsec-head" },
|
|
887
|
+
h("span", { className: "lm-setsec-headtitle" }, "PhoneLens 手机相机"),
|
|
888
|
+
h("span", { className: "lm-setsec-badge" }, "phone-lens"),
|
|
889
|
+
),
|
|
890
|
+
h("div", { className: "lm-setsec-intro" }, "管理对话中模型对本机手机相机的调用能力与隐私行为。"),
|
|
891
|
+
st === undefined
|
|
892
|
+
? h("div", { className: "lm-setsec-note" }, "加载中…")
|
|
893
|
+
: st === false
|
|
894
|
+
? h("div", { className: "lm-setsec-note" }, "设置服务不可用(需电脑端 phone-lens ≥ 1.0.4)")
|
|
895
|
+
: [
|
|
896
|
+
h(
|
|
897
|
+
"div",
|
|
898
|
+
{ className: "lm-setsec-card", key: "privacy" },
|
|
899
|
+
h("div", { className: "lm-setsec-cardhead" }, "隐私开关"),
|
|
900
|
+
h(
|
|
901
|
+
"div",
|
|
902
|
+
{ className: "lm-setsec-rows" },
|
|
903
|
+
h(LensSetRow, {
|
|
904
|
+
key: "t1",
|
|
905
|
+
title: "启用模型相机工具",
|
|
906
|
+
desc: "关闭后模型调用会被拒绝,手机摄像头完全由你自己控制",
|
|
907
|
+
}, h(LensSwitch, { checked: st.modelToolsEnabled, onChange: (v) => void patch({ modelToolsEnabled: v }) })),
|
|
908
|
+
h(LensSetRow, {
|
|
909
|
+
key: "t2",
|
|
910
|
+
title: "模型免确认调用",
|
|
911
|
+
desc: "开启时模型调用不弹确认框;关闭后每次调用需你在弹窗中确认(若宿主不支持审批弹窗,则视为拒绝)",
|
|
912
|
+
}, h(LensSwitch, { checked: st.modelToolsConfirmFree, onChange: (v) => void patch({ modelToolsConfirmFree: v }) })),
|
|
913
|
+
// pointer to the high-frequency settings, parked INSIDE
|
|
914
|
+
// the toggles card (user-requested placement)
|
|
915
|
+
h(
|
|
916
|
+
"div",
|
|
917
|
+
{ className: "lm-set-row", key: "ft" },
|
|
918
|
+
h("span", { className: "lm-set-rowtext" }, h("span", { className: "lm-set-rowdesc" }, "高频的「拍照注入模式 / 保存目录」设置仍在右下角悬浮窗 ⚙ 面板")),
|
|
919
|
+
),
|
|
920
|
+
),
|
|
921
|
+
),
|
|
922
|
+
h(
|
|
923
|
+
"div",
|
|
924
|
+
{ className: "lm-setsec-card", key: "what" },
|
|
925
|
+
h("div", { className: "lm-setsec-cardhead" }, "打开上述开关后,对话内模型将被允许:"),
|
|
926
|
+
h(
|
|
927
|
+
"div",
|
|
928
|
+
{ className: "lm-setsec-rows" },
|
|
929
|
+
// tool names in the open (user-requested): what the
|
|
930
|
+
// model will actually see in its tool list
|
|
931
|
+
infoRow("拍摄照片(phone_take_photo)", "手机立即拍照,照片直接进入模型视野——不放入输入框、不保存到电脑文件夹,与拍照注入模式设置无关。", "w1"),
|
|
932
|
+
infoRow("关闭手机摄像头(phone_camera_pause)", "让手机相机提前进入空闲省电状态。", "w2"),
|
|
933
|
+
infoRow("恢复手机摄像头(phone_camera_resume)", "立即打开相机并恢复预览拍摄。", "w3"),
|
|
934
|
+
),
|
|
935
|
+
),
|
|
936
|
+
flash ? h("div", { className: "lm-setsec-note", key: "f" }, flash) : null,
|
|
937
|
+
],
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// ── settings nav icon ─────────────────────────────────────────────────
|
|
942
|
+
// settings.section alone yields the nav row; this paints a camera icon
|
|
943
|
+
// onto it with the same CSS-mask trick dshmarket uses (mark the row
|
|
944
|
+
// whose text matches our label, inject a ::before mask).
|
|
945
|
+
const LENS_NAV_SVG =
|
|
946
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3.5"/></svg>';
|
|
947
|
+
|
|
948
|
+
function installSettingsNavIcon(ctx, resolveLabel) {
|
|
949
|
+
if (typeof document === "undefined") return;
|
|
950
|
+
const MARKER = "data-phone-lens-nav-icon";
|
|
951
|
+
const SELECTOR = '[role="dialog"] nav button';
|
|
952
|
+
const maskUrl = `data:image/svg+xml,${encodeURIComponent(LENS_NAV_SVG)}`;
|
|
953
|
+
ctx.effect(() => {
|
|
954
|
+
const tag = document.createElement("style");
|
|
955
|
+
tag.dataset.plugin = "phone-lens";
|
|
956
|
+
tag.textContent = [
|
|
957
|
+
`[${MARKER}] > svg { display: none; }`,
|
|
958
|
+
`[${MARKER}]::before {`,
|
|
959
|
+
` content: '';`,
|
|
960
|
+
` flex: none;`,
|
|
961
|
+
` width: 16px;`,
|
|
962
|
+
` height: 16px;`,
|
|
963
|
+
` background-color: currentColor;`,
|
|
964
|
+
` -webkit-mask-image: url("${maskUrl}");`,
|
|
965
|
+
` mask-image: url("${maskUrl}");`,
|
|
966
|
+
` -webkit-mask-repeat: no-repeat;`,
|
|
967
|
+
` mask-repeat: no-repeat;`,
|
|
968
|
+
` -webkit-mask-position: center;`,
|
|
969
|
+
` mask-position: center;`,
|
|
970
|
+
` -webkit-mask-size: 16px 16px;`,
|
|
971
|
+
` mask-size: 16px 16px;`,
|
|
972
|
+
`}`,
|
|
973
|
+
].join("\n");
|
|
974
|
+
document.head.appendChild(tag);
|
|
975
|
+
let disposed = false;
|
|
976
|
+
let scheduled = false;
|
|
977
|
+
const sync = () => {
|
|
978
|
+
scheduled = false;
|
|
979
|
+
if (disposed) return;
|
|
980
|
+
// cheap early-out: the settings dialog isn't even mounted — skip
|
|
981
|
+
// the querySelectorAll sweep while chat tokens stream in
|
|
982
|
+
if (!document.querySelector('[role="dialog"] nav')) return;
|
|
983
|
+
const wanted = resolveLabel();
|
|
984
|
+
for (const row of document.querySelectorAll(SELECTOR)) {
|
|
985
|
+
if ((row.textContent || "").trim() === wanted) row.setAttribute(MARKER, "");
|
|
986
|
+
else row.removeAttribute(MARKER);
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
const schedule = () => {
|
|
990
|
+
if (scheduled || disposed) return;
|
|
991
|
+
scheduled = true;
|
|
992
|
+
queueMicrotask(sync);
|
|
993
|
+
};
|
|
994
|
+
sync();
|
|
995
|
+
const observer = new MutationObserver(schedule);
|
|
996
|
+
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
997
|
+
return () => {
|
|
998
|
+
disposed = true;
|
|
999
|
+
observer.disconnect();
|
|
1000
|
+
for (const row of document.querySelectorAll(`[${MARKER}]`)) row.removeAttribute(MARKER);
|
|
1001
|
+
tag.remove();
|
|
1002
|
+
};
|
|
1003
|
+
}, "phone-lens: settings nav icon");
|
|
1004
|
+
}
|
|
1005
|
+
|
|
714
1006
|
// ── plugin face ───────────────────────────────────────────────────────
|
|
715
1007
|
const inject = ["slots"];
|
|
716
1008
|
function apply(ctx) {
|
|
@@ -721,6 +1013,15 @@ window.__ModuleLoader__.load({
|
|
|
721
1013
|
LensOverlay,
|
|
722
1014
|
),
|
|
723
1015
|
);
|
|
1016
|
+
// official settings page: additive section — the host renders it as a
|
|
1017
|
+
// dedicated entry in the settings dialog's left nav (same as dshmarket)
|
|
1018
|
+
ctx.slots.inject("settings.section", () =>
|
|
1019
|
+
ctx.slots.register(
|
|
1020
|
+
{ name: "settings.section", id: "phone-lens", order: 50, label: () => "PhoneLens" },
|
|
1021
|
+
() => h(LensSettingsSection),
|
|
1022
|
+
),
|
|
1023
|
+
);
|
|
1024
|
+
installSettingsNavIcon(ctx, () => "PhoneLens");
|
|
724
1025
|
}
|
|
725
1026
|
|
|
726
1027
|
exports.apply = apply;
|