phone-lens 1.0.3 → 1.1.0
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 +300 -9
- package/lib/dev.js +290 -17
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +414 -22
- 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,7 +260,11 @@ 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)
|
|
@@ -251,6 +280,26 @@ window.__ModuleLoader__.load({
|
|
|
251
280
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
252
281
|
const wsUrl = `ws://127.0.0.1:${port}/ws/view`;
|
|
253
282
|
|
|
283
|
+
// plugin-update check, once per panel-open (host caches the Gitee
|
|
284
|
+
// lookup 12h / failures 30min — this fetch never hits the API itself)
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
if (!open) return;
|
|
287
|
+
let alive = true;
|
|
288
|
+
void (async () => {
|
|
289
|
+
try {
|
|
290
|
+
const r = await fetch(`${baseUrl}/update-check`, { cache: "no-store" });
|
|
291
|
+
if (!r.ok) throw new Error(String(r.status));
|
|
292
|
+
const data = await r.json();
|
|
293
|
+
if (alive) setUpd(data && data.current ? data : null);
|
|
294
|
+
} catch {
|
|
295
|
+
if (alive) setUpd(null);
|
|
296
|
+
}
|
|
297
|
+
})();
|
|
298
|
+
return () => {
|
|
299
|
+
alive = false;
|
|
300
|
+
};
|
|
301
|
+
}, [open, port]);
|
|
302
|
+
|
|
254
303
|
// view websocket: ALWAYS connected (independent of panel open state),
|
|
255
304
|
// so the connection status and pending-photo events refresh even when
|
|
256
305
|
// the FAB is collapsed. Frames are only drawn when the canvas exists.
|
|
@@ -313,12 +362,13 @@ window.__ModuleLoader__.load({
|
|
|
313
362
|
})
|
|
314
363
|
.catch((e) => setFlashErr("自动放入失败: " + String(e && e.message || e).slice(0, 90)));
|
|
315
364
|
} else if (m.type === "devices") {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
365
|
+
// 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);
|
|
368
|
+
setDevices(m.devices || []);
|
|
369
|
+
// a NEW device came online → the one-time pairing code was
|
|
370
|
+
// consumed; rotate the QR so the next scan sees a fresh code
|
|
371
|
+
if (grew) void refreshQr(true);
|
|
322
372
|
// an active device present == camera linked; derive both the
|
|
323
373
|
// canvas visibility and the header/FAB link state from the
|
|
324
374
|
// device list (host no longer emits `device`)
|
|
@@ -547,6 +597,21 @@ window.__ModuleLoader__.load({
|
|
|
547
597
|
: "预览中",
|
|
548
598
|
),
|
|
549
599
|
),
|
|
600
|
+
// ── version hints (plain colored lines, never dialogs) ──
|
|
601
|
+
upd && upd.updateAvailable
|
|
602
|
+
? h("div", { className: "lm-updwarn", key: "upd" }, `⬆ 电脑端有新版本 v${upd.latest} — 请在 设置 → 插件市场 更新`)
|
|
603
|
+
: null,
|
|
604
|
+
(() => {
|
|
605
|
+
// cross-end consistency: the phone reports its App version
|
|
606
|
+
// in hello; a mismatch (both ends known) warms the line
|
|
607
|
+
const app = (devices || []).find((d) => d.active)?.appVersion;
|
|
608
|
+
if (!app) return null;
|
|
609
|
+
const host = upd && upd.current;
|
|
610
|
+
if (host && app !== host) {
|
|
611
|
+
return h("div", { className: "lm-appver mismatch", key: "appver" }, `手机 App v${app} 与电脑端 v${host} 版本不一致,建议两端同时更新`);
|
|
612
|
+
}
|
|
613
|
+
return h("div", { className: "lm-appver", key: "appver" }, `手机 App v${app}`);
|
|
614
|
+
})(),
|
|
550
615
|
devices && devices.length > 1
|
|
551
616
|
? h(
|
|
552
617
|
"div",
|
|
@@ -711,6 +776,223 @@ window.__ModuleLoader__.load({
|
|
|
711
776
|
);
|
|
712
777
|
}
|
|
713
778
|
|
|
779
|
+
// ── settings.section — the official settings-page panel ───────────────
|
|
780
|
+
// Privacy-bearing switches live here with an honest description of what
|
|
781
|
+
// the model can do and where photos go. The high-frequency capture-mode
|
|
782
|
+
// settings stay in the overlay ⚙ panel on purpose (switched constantly).
|
|
783
|
+
function currentLensPort() {
|
|
784
|
+
try {
|
|
785
|
+
return window.localStorage.getItem(PORT_KEY) || "8791";
|
|
786
|
+
} catch {
|
|
787
|
+
return "8791";
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Custom switch per the DSH settings-row recipe: a real checkbox (native
|
|
792
|
+
// semantics + focus) driving a styled track/thumb — 36×20 track, 14px
|
|
793
|
+
// thumb, primary fill when checked. Sizes/tokens mirror dsh-better-sidebar.
|
|
794
|
+
function LensSwitch({ checked, onChange }) {
|
|
795
|
+
return h(
|
|
796
|
+
"label",
|
|
797
|
+
{ className: "lm-set-switch" },
|
|
798
|
+
h("input", {
|
|
799
|
+
type: "checkbox",
|
|
800
|
+
checked: !!checked,
|
|
801
|
+
onChange: (e) => onChange(e.target.checked),
|
|
802
|
+
}),
|
|
803
|
+
h("span", { className: "lm-set-track", "aria-hidden": "true" }, h("span", { className: "lm-set-thumb" })),
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// One settings row: title/desc on the left, control on the right.
|
|
808
|
+
function LensSetRow({ title, desc, children }) {
|
|
809
|
+
return h(
|
|
810
|
+
"div",
|
|
811
|
+
{ className: "lm-set-row" },
|
|
812
|
+
h(
|
|
813
|
+
"span",
|
|
814
|
+
{ className: "lm-set-rowtext" },
|
|
815
|
+
h("span", { className: "lm-set-rowtitle" }, title),
|
|
816
|
+
desc ? h("span", { className: "lm-set-rowdesc" }, desc) : null,
|
|
817
|
+
),
|
|
818
|
+
h("span", { className: "lm-set-control" }, children),
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function LensSettingsSection() {
|
|
823
|
+
const [st, setSt] = useState(undefined); // undefined=loading, false=unavailable, object=live
|
|
824
|
+
const [flash, setFlash] = useState("");
|
|
825
|
+
|
|
826
|
+
useEffect(() => {
|
|
827
|
+
let alive = true;
|
|
828
|
+
void (async () => {
|
|
829
|
+
try {
|
|
830
|
+
const r = await fetch(`http://127.0.0.1:${currentLensPort()}/app-settings`, { cache: "no-store" });
|
|
831
|
+
if (!r.ok) throw new Error(String(r.status));
|
|
832
|
+
const data = await r.json();
|
|
833
|
+
if (alive) setSt(data);
|
|
834
|
+
} catch {
|
|
835
|
+
if (alive) setSt(false);
|
|
836
|
+
}
|
|
837
|
+
})();
|
|
838
|
+
return () => {
|
|
839
|
+
alive = false;
|
|
840
|
+
};
|
|
841
|
+
}, []);
|
|
842
|
+
|
|
843
|
+
async function patch(p) {
|
|
844
|
+
try {
|
|
845
|
+
const r = await fetch(`http://127.0.0.1:${currentLensPort()}/app-settings`, {
|
|
846
|
+
method: "POST",
|
|
847
|
+
headers: { "content-type": "application/json" },
|
|
848
|
+
body: JSON.stringify(p),
|
|
849
|
+
});
|
|
850
|
+
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
851
|
+
setSt(await r.json());
|
|
852
|
+
setFlash("已保存");
|
|
853
|
+
setTimeout(() => setFlash(""), 1800);
|
|
854
|
+
} catch (e) {
|
|
855
|
+
setFlash("保存失败: " + String((e && e.message) || e).slice(0, 60));
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const infoRow = (title, desc, key) =>
|
|
860
|
+
h(
|
|
861
|
+
"div",
|
|
862
|
+
{ className: "lm-set-row", key },
|
|
863
|
+
h(
|
|
864
|
+
"span",
|
|
865
|
+
{ className: "lm-set-rowtext" },
|
|
866
|
+
h("span", { className: "lm-set-rowtitle" }, title),
|
|
867
|
+
h("span", { className: "lm-set-rowdesc" }, desc),
|
|
868
|
+
),
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
return h(
|
|
872
|
+
"div",
|
|
873
|
+
{ className: "lm-setsec" },
|
|
874
|
+
h(
|
|
875
|
+
"div",
|
|
876
|
+
{ className: "lm-setsec-head" },
|
|
877
|
+
h("span", { className: "lm-setsec-headtitle" }, "PhoneLens 手机相机"),
|
|
878
|
+
h("span", { className: "lm-setsec-badge" }, "phone-lens"),
|
|
879
|
+
),
|
|
880
|
+
h("div", { className: "lm-setsec-intro" }, "管理对话中模型对本机手机相机的调用能力与隐私行为。"),
|
|
881
|
+
st === undefined
|
|
882
|
+
? h("div", { className: "lm-setsec-note" }, "加载中…")
|
|
883
|
+
: st === false
|
|
884
|
+
? h("div", { className: "lm-setsec-note" }, "设置服务不可用(需电脑端 phone-lens ≥ 1.0.4)")
|
|
885
|
+
: [
|
|
886
|
+
h(
|
|
887
|
+
"div",
|
|
888
|
+
{ className: "lm-setsec-card", key: "privacy" },
|
|
889
|
+
h("div", { className: "lm-setsec-cardhead" }, "隐私开关"),
|
|
890
|
+
h(
|
|
891
|
+
"div",
|
|
892
|
+
{ className: "lm-setsec-rows" },
|
|
893
|
+
h(LensSetRow, {
|
|
894
|
+
key: "t1",
|
|
895
|
+
title: "启用模型相机工具",
|
|
896
|
+
desc: "关闭后模型调用会被拒绝,手机摄像头完全由你自己控制",
|
|
897
|
+
}, h(LensSwitch, { checked: st.modelToolsEnabled, onChange: (v) => void patch({ modelToolsEnabled: v }) })),
|
|
898
|
+
h(LensSetRow, {
|
|
899
|
+
key: "t2",
|
|
900
|
+
title: "模型免确认调用",
|
|
901
|
+
desc: "开启时模型调用不弹确认框;关闭后每次调用需你在弹窗中确认(若宿主不支持审批弹窗,则视为拒绝)",
|
|
902
|
+
}, h(LensSwitch, { checked: st.modelToolsConfirmFree, onChange: (v) => void patch({ modelToolsConfirmFree: v }) })),
|
|
903
|
+
// pointer to the high-frequency settings, parked INSIDE
|
|
904
|
+
// the toggles card (user-requested placement)
|
|
905
|
+
h(
|
|
906
|
+
"div",
|
|
907
|
+
{ className: "lm-set-row", key: "ft" },
|
|
908
|
+
h("span", { className: "lm-set-rowtext" }, h("span", { className: "lm-set-rowdesc" }, "高频的「拍照注入模式 / 保存目录」设置仍在右下角悬浮窗 ⚙ 面板")),
|
|
909
|
+
),
|
|
910
|
+
),
|
|
911
|
+
),
|
|
912
|
+
h(
|
|
913
|
+
"div",
|
|
914
|
+
{ className: "lm-setsec-card", key: "what" },
|
|
915
|
+
h("div", { className: "lm-setsec-cardhead" }, "打开上述开关后,对话内模型将被允许:"),
|
|
916
|
+
h(
|
|
917
|
+
"div",
|
|
918
|
+
{ className: "lm-setsec-rows" },
|
|
919
|
+
// tool names in the open (user-requested): what the
|
|
920
|
+
// 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"),
|
|
924
|
+
),
|
|
925
|
+
),
|
|
926
|
+
flash ? h("div", { className: "lm-setsec-note", key: "f" }, flash) : null,
|
|
927
|
+
],
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// ── settings nav icon ─────────────────────────────────────────────────
|
|
932
|
+
// settings.section alone yields the nav row; this paints a camera icon
|
|
933
|
+
// onto it with the same CSS-mask trick dshmarket uses (mark the row
|
|
934
|
+
// whose text matches our label, inject a ::before mask).
|
|
935
|
+
const LENS_NAV_SVG =
|
|
936
|
+
'<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>';
|
|
937
|
+
|
|
938
|
+
function installSettingsNavIcon(ctx, resolveLabel) {
|
|
939
|
+
if (typeof document === "undefined") return;
|
|
940
|
+
const MARKER = "data-phone-lens-nav-icon";
|
|
941
|
+
const SELECTOR = '[role="dialog"] nav button';
|
|
942
|
+
const maskUrl = `data:image/svg+xml,${encodeURIComponent(LENS_NAV_SVG)}`;
|
|
943
|
+
ctx.effect(() => {
|
|
944
|
+
const tag = document.createElement("style");
|
|
945
|
+
tag.dataset.plugin = "phone-lens";
|
|
946
|
+
tag.textContent = [
|
|
947
|
+
`[${MARKER}] > svg { display: none; }`,
|
|
948
|
+
`[${MARKER}]::before {`,
|
|
949
|
+
` content: '';`,
|
|
950
|
+
` flex: none;`,
|
|
951
|
+
` width: 16px;`,
|
|
952
|
+
` height: 16px;`,
|
|
953
|
+
` background-color: currentColor;`,
|
|
954
|
+
` -webkit-mask-image: url("${maskUrl}");`,
|
|
955
|
+
` mask-image: url("${maskUrl}");`,
|
|
956
|
+
` -webkit-mask-repeat: no-repeat;`,
|
|
957
|
+
` mask-repeat: no-repeat;`,
|
|
958
|
+
` -webkit-mask-position: center;`,
|
|
959
|
+
` mask-position: center;`,
|
|
960
|
+
` -webkit-mask-size: 16px 16px;`,
|
|
961
|
+
` mask-size: 16px 16px;`,
|
|
962
|
+
`}`,
|
|
963
|
+
].join("\n");
|
|
964
|
+
document.head.appendChild(tag);
|
|
965
|
+
let disposed = false;
|
|
966
|
+
let scheduled = false;
|
|
967
|
+
const sync = () => {
|
|
968
|
+
scheduled = false;
|
|
969
|
+
if (disposed) return;
|
|
970
|
+
// cheap early-out: the settings dialog isn't even mounted — skip
|
|
971
|
+
// the querySelectorAll sweep while chat tokens stream in
|
|
972
|
+
if (!document.querySelector('[role="dialog"] nav')) return;
|
|
973
|
+
const wanted = resolveLabel();
|
|
974
|
+
for (const row of document.querySelectorAll(SELECTOR)) {
|
|
975
|
+
if ((row.textContent || "").trim() === wanted) row.setAttribute(MARKER, "");
|
|
976
|
+
else row.removeAttribute(MARKER);
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
const schedule = () => {
|
|
980
|
+
if (scheduled || disposed) return;
|
|
981
|
+
scheduled = true;
|
|
982
|
+
queueMicrotask(sync);
|
|
983
|
+
};
|
|
984
|
+
sync();
|
|
985
|
+
const observer = new MutationObserver(schedule);
|
|
986
|
+
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
987
|
+
return () => {
|
|
988
|
+
disposed = true;
|
|
989
|
+
observer.disconnect();
|
|
990
|
+
for (const row of document.querySelectorAll(`[${MARKER}]`)) row.removeAttribute(MARKER);
|
|
991
|
+
tag.remove();
|
|
992
|
+
};
|
|
993
|
+
}, "phone-lens: settings nav icon");
|
|
994
|
+
}
|
|
995
|
+
|
|
714
996
|
// ── plugin face ───────────────────────────────────────────────────────
|
|
715
997
|
const inject = ["slots"];
|
|
716
998
|
function apply(ctx) {
|
|
@@ -721,6 +1003,15 @@ window.__ModuleLoader__.load({
|
|
|
721
1003
|
LensOverlay,
|
|
722
1004
|
),
|
|
723
1005
|
);
|
|
1006
|
+
// official settings page: additive section — the host renders it as a
|
|
1007
|
+
// dedicated entry in the settings dialog's left nav (same as dshmarket)
|
|
1008
|
+
ctx.slots.inject("settings.section", () =>
|
|
1009
|
+
ctx.slots.register(
|
|
1010
|
+
{ name: "settings.section", id: "phone-lens", order: 50, label: () => "PhoneLens" },
|
|
1011
|
+
() => h(LensSettingsSection),
|
|
1012
|
+
),
|
|
1013
|
+
);
|
|
1014
|
+
installSettingsNavIcon(ctx, () => "PhoneLens");
|
|
724
1015
|
}
|
|
725
1016
|
|
|
726
1017
|
exports.apply = apply;
|