dsh-notify-me 1.7.0 → 1.9.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/CHANGELOG.md +53 -0
- package/README.en.md +204 -63
- package/README.md +201 -60
- package/lib/client.js +515 -169
- package/package.json +9 -4
package/lib/client.js
CHANGED
|
@@ -256,6 +256,19 @@
|
|
|
256
256
|
// through the Service Worker half of this same file; where that cannot
|
|
257
257
|
// run, the toast simply falls back to its plain form.
|
|
258
258
|
quickActions: true,
|
|
259
|
+
// Where the alert is drawn. The OS toast always lands on the primary
|
|
260
|
+
// display — Windows offers no setting (and Chromium no API) to aim a
|
|
261
|
+
// banner at another monitor — so a user whose DSH window lives on a
|
|
262
|
+
// second screen never sees one. This option draws the same alert inside
|
|
263
|
+
// the page instead: it appears on whatever screen the window is on, needs
|
|
264
|
+
// no notification permission, and can carry the approval buttons even on
|
|
265
|
+
// the desktop shell (whose dsh-app:// page cannot run the Service Worker
|
|
266
|
+
// that OS action buttons need). Independent of `toast`; neither replaces
|
|
267
|
+
// the other, so "both" is a valid setup.
|
|
268
|
+
overlay: false,
|
|
269
|
+
// Which corner of this window the overlay cards stack in. The screen
|
|
270
|
+
// follows the window — the page cannot aim it anywhere else either.
|
|
271
|
+
overlayCorner: "bottom-right", // bottom-right | bottom-left | top-right | top-left
|
|
259
272
|
// External forwarding: mirror every alert the plugin actually delivers to
|
|
260
273
|
// webhook endpoints (Discord / Bark / Gotify / custom), so a phone or
|
|
261
274
|
// another box can tell the user to come back. Off by default — nothing
|
|
@@ -287,6 +300,12 @@
|
|
|
287
300
|
function setConfigLocal(patch) {
|
|
288
301
|
config = writeConfig(patch);
|
|
289
302
|
applyTitle();
|
|
303
|
+
// Overlay re-apply: switching it off takes the cards down at once and a
|
|
304
|
+
// new corner moves the layer without waiting for the next alert.
|
|
305
|
+
try {
|
|
306
|
+
if (!config.overlay) clearOverlays();
|
|
307
|
+
else if (overlayLayer) overlayLayer.className = "dnm-ov-layer dnm-ov-" + overlayCorner();
|
|
308
|
+
} catch (e) {}
|
|
290
309
|
return config;
|
|
291
310
|
}
|
|
292
311
|
|
|
@@ -341,9 +360,10 @@
|
|
|
341
360
|
sep: ":",
|
|
342
361
|
approve: "同意",
|
|
343
362
|
reject: "拒绝",
|
|
363
|
+
dismiss: "关闭这条提醒",
|
|
344
364
|
testAttention: "这是一条测试提醒:DSH 需要你的操作",
|
|
345
365
|
testDone: "测试提醒 · 回复完成示例",
|
|
346
|
-
testApproval: "
|
|
366
|
+
testApproval: "这是一条测试审批:点「同意 / 拒绝」试试快捷裁决",
|
|
347
367
|
testApproved: "测试提醒 · 已模拟「同意」(仅测试,未改动任何会话)",
|
|
348
368
|
testRejected: "测试提醒 · 已模拟「拒绝」(仅测试,未改动任何会话)",
|
|
349
369
|
testExternalTitle: "外部通知测试",
|
|
@@ -367,9 +387,10 @@
|
|
|
367
387
|
sep: ": ",
|
|
368
388
|
approve: "Approve",
|
|
369
389
|
reject: "Reject",
|
|
390
|
+
dismiss: "Dismiss this alert",
|
|
370
391
|
testAttention: "This is a test alert: DSH is waiting for your input",
|
|
371
392
|
testDone: "Test alert · reply-finished sample",
|
|
372
|
-
testApproval: "This is a test approval: try
|
|
393
|
+
testApproval: "This is a test approval: try the Approve / Reject buttons to settle it",
|
|
373
394
|
testApproved: "Test alert · simulated \"Approve\" (test only, nothing changed)",
|
|
374
395
|
testRejected: "Test alert · simulated \"Reject\" (test only, nothing changed)",
|
|
375
396
|
testExternalTitle: "External notification test",
|
|
@@ -639,6 +660,7 @@
|
|
|
639
660
|
if (!rec) return false; // already answered elsewhere, or a stale toast
|
|
640
661
|
delete pendingActions[key];
|
|
641
662
|
if (!rec.test) closeNotificationTag(rec.tag);
|
|
663
|
+
closeOverlayForKey(key);
|
|
642
664
|
if (rec.test) {
|
|
643
665
|
// Test toasts settle nothing real; the feedback toast IS the proof that
|
|
644
666
|
// the button clicked all the way through the worker bridge.
|
|
@@ -660,7 +682,10 @@
|
|
|
660
682
|
// they are the only kind whose decision is a plain binary, and the only one
|
|
661
683
|
// whose object answers answer('allowed-once' | 'rejected').
|
|
662
684
|
function quickFor(key, it, sessionId) {
|
|
663
|
-
|
|
685
|
+
// The overlay can settle approvals without a worker, so it counts as a
|
|
686
|
+
// button channel on its own — the desktop shell, where OS action buttons
|
|
687
|
+
// cannot render, gets them this way.
|
|
688
|
+
if (!config.quickActions || !(config.toast || config.overlay)) return null;
|
|
664
689
|
if (!it || it.kind !== "approval" || typeof it.answer !== "function") return null;
|
|
665
690
|
var q = {
|
|
666
691
|
key: key,
|
|
@@ -734,6 +759,191 @@
|
|
|
734
759
|
setTimeout(function () { n.close(); }, 15000);
|
|
735
760
|
} catch (e) {}
|
|
736
761
|
}
|
|
762
|
+
|
|
763
|
+
// ─────────────────── page overlay reminders ──────────────────────
|
|
764
|
+
// Windows draws notification banners on the primary display and exposes no
|
|
765
|
+
// way to aim them at another monitor; a user who keeps DSH on a second
|
|
766
|
+
// screen therefore never sees a toast. This overlay draws the same alert
|
|
767
|
+
// inside the page instead, so it lands on whatever screen the DSH window
|
|
768
|
+
// occupies. It also needs no notification permission, and — unlike OS
|
|
769
|
+
// action buttons, which require the Service Worker the desktop shell cannot
|
|
770
|
+
// run — it can carry the 同意/拒绝 buttons everywhere. Off by default;
|
|
771
|
+
// independent of the toast switch.
|
|
772
|
+
var OVERLAY_STYLE_ID = "dsh-notify-me/overlay";
|
|
773
|
+
var OVERLAY_ID = "dsh-notify-me-overlay";
|
|
774
|
+
var OVERLAY_MAX_CARDS = 4;
|
|
775
|
+
// An approval card is actionable and stays until it is answered or closed;
|
|
776
|
+
// the other kinds fade on the toast's own schedule (a toast closes at 15s).
|
|
777
|
+
var OVERLAY_TTL = { attention: 20000, done: 8000 };
|
|
778
|
+
var OVERLAY_CORNERS = ["bottom-right", "bottom-left", "top-right", "top-left"];
|
|
779
|
+
var OVERLAY_CSS = "" +
|
|
780
|
+
".dnm-ov-layer{position:fixed;z-index:2147483000;display:flex;flex-direction:column;gap:10px;pointer-events:none;max-width:calc(100vw - 24px)}" +
|
|
781
|
+
".dnm-ov-bottom-right{right:16px;bottom:16px;align-items:flex-end}" +
|
|
782
|
+
".dnm-ov-bottom-left{left:16px;bottom:16px;align-items:flex-start}" +
|
|
783
|
+
".dnm-ov-top-right{right:16px;top:16px;align-items:flex-end}" +
|
|
784
|
+
".dnm-ov-top-left{left:16px;top:16px;align-items:flex-start}" +
|
|
785
|
+
".dnm-ov{pointer-events:auto;width:320px;max-width:calc(100vw - 32px);box-sizing:border-box;border-radius:12px;padding:10px 12px 11px;display:flex;flex-direction:column;gap:7px;font-size:13px;line-height:19px;color:var(--dsw-alias-label-primary,#f3f4f6);background:var(--dsw-alias-bg-layer-3,rgba(30,32,36,.97));border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.35));border-left-width:3px;box-shadow:0 12px 32px rgba(0,0,0,.32);cursor:pointer;animation:dnmOvIn .16s ease-out}" +
|
|
786
|
+
".dnm-ov-attention{border-left-color:var(--dsw-alias-state-warning-primary,#f59e0b)}" +
|
|
787
|
+
".dnm-ov-done{border-left-color:var(--dsw-alias-state-success-primary,#22c55e)}" +
|
|
788
|
+
".dnm-ov-head{display:flex;align-items:center;gap:7px}" +
|
|
789
|
+
".dnm-ov-dot{flex:none;width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.55}" +
|
|
790
|
+
".dnm-ov-attention .dnm-ov-dot{color:var(--dsw-alias-state-warning-primary,#f59e0b);opacity:1}" +
|
|
791
|
+
".dnm-ov-done .dnm-ov-dot{color:var(--dsw-alias-state-success-primary,#22c55e);opacity:1}" +
|
|
792
|
+
".dnm-ov-title{flex:1;min-width:0;font-weight:600;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}" +
|
|
793
|
+
".dnm-ov-x{flex:none;border:0;background:transparent;color:inherit;opacity:.6;font-size:15px;line-height:15px;padding:0 2px;cursor:pointer;font-family:inherit}" +
|
|
794
|
+
".dnm-ov-x:hover{opacity:1}" +
|
|
795
|
+
".dnm-ov-body{color:var(--dsw-alias-label-secondary,rgba(220,222,226,.86));font-size:12px;line-height:18px;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word}" +
|
|
796
|
+
".dnm-ov-actions{display:flex;gap:8px;margin-top:2px}" +
|
|
797
|
+
".dnm-ov-btn{flex:1;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4));background:var(--dsw-alias-bg-layer-1,rgba(255,255,255,.06));color:inherit;border-radius:8px;padding:5px 10px;font-size:12px;font-family:inherit;cursor:pointer}" +
|
|
798
|
+
".dnm-ov-btn:hover{background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.12))}" +
|
|
799
|
+
".dnm-ov-approve{border-color:transparent;background:var(--dsw-alias-state-business-primary,#3b82f6);color:#fff}" +
|
|
800
|
+
".dnm-ov-approve:hover{background:var(--dsw-alias-state-business-primary,#3b82f6);filter:brightness(1.06)}" +
|
|
801
|
+
".dnm-ov-reject{color:inherit}" +
|
|
802
|
+
"@keyframes dnmOvIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}";
|
|
803
|
+
var overlayLayer = null;
|
|
804
|
+
var overlayCards = Object.create(null); // id -> { card, key, kind, timer }
|
|
805
|
+
var overlaySeq = 0;
|
|
806
|
+
function overlayCorner() {
|
|
807
|
+
return OVERLAY_CORNERS.indexOf(config.overlayCorner) === -1 ? "bottom-right" : config.overlayCorner;
|
|
808
|
+
}
|
|
809
|
+
function injectOverlayStyle() {
|
|
810
|
+
try {
|
|
811
|
+
if (typeof document === "undefined" || typeof document.createElement !== "function") return;
|
|
812
|
+
if (!document.head && !document.documentElement) return;
|
|
813
|
+
var sel = "style[data-plugin-css=" + JSON.stringify(OVERLAY_STYLE_ID) + "]";
|
|
814
|
+
var seen = false;
|
|
815
|
+
try { seen = !!(document.querySelector && document.querySelector(sel)); } catch (e) {}
|
|
816
|
+
if (seen) return;
|
|
817
|
+
var s = document.createElement("style");
|
|
818
|
+
s.dataset.plugin = "dsh-notify-me";
|
|
819
|
+
s.dataset.pluginCss = OVERLAY_STYLE_ID;
|
|
820
|
+
s.textContent = OVERLAY_CSS;
|
|
821
|
+
(document.head || document.documentElement).appendChild(s);
|
|
822
|
+
} catch (e) {}
|
|
823
|
+
}
|
|
824
|
+
// The fixed layer lives directly under <body> so no ancestor transform or
|
|
825
|
+
// stacking context in the host app can drag the cards away from the corner.
|
|
826
|
+
function ensureOverlayLayer() {
|
|
827
|
+
try {
|
|
828
|
+
if (typeof document === "undefined" || typeof document.createElement !== "function" || !document.body) return null;
|
|
829
|
+
injectOverlayStyle();
|
|
830
|
+
if (!overlayLayer || !overlayLayer.parentNode) {
|
|
831
|
+
// A reload of this bundle can leave a previous layer behind; the id is
|
|
832
|
+
// the only handle an HMR-free page gives us.
|
|
833
|
+
var old = null;
|
|
834
|
+
try { old = document.getElementById && document.getElementById(OVERLAY_ID); } catch (e) {}
|
|
835
|
+
if (old && old.parentNode) old.parentNode.removeChild(old);
|
|
836
|
+
overlayLayer = document.createElement("div");
|
|
837
|
+
overlayLayer.id = OVERLAY_ID;
|
|
838
|
+
overlayLayer.setAttribute("role", "status");
|
|
839
|
+
overlayLayer.setAttribute("aria-live", "polite");
|
|
840
|
+
document.body.appendChild(overlayLayer);
|
|
841
|
+
}
|
|
842
|
+
overlayLayer.className = "dnm-ov-layer dnm-ov-" + overlayCorner();
|
|
843
|
+
return overlayLayer;
|
|
844
|
+
} catch (e) { return null; }
|
|
845
|
+
}
|
|
846
|
+
function dismissOverlay(id) {
|
|
847
|
+
var rec = overlayCards[id];
|
|
848
|
+
if (!rec) return;
|
|
849
|
+
delete overlayCards[id];
|
|
850
|
+
if (rec.timer) { try { clearTimeout(rec.timer); } catch (e) {} }
|
|
851
|
+
try { if (rec.card && rec.card.parentNode) rec.card.parentNode.removeChild(rec.card); } catch (e) {}
|
|
852
|
+
}
|
|
853
|
+
// An approval that gets answered elsewhere (the in-page card, another
|
|
854
|
+
// toast, a session switch) must not leave a stale overlay behind.
|
|
855
|
+
function closeOverlayForKey(key) {
|
|
856
|
+
if (!key) return;
|
|
857
|
+
var ids = Object.keys(overlayCards);
|
|
858
|
+
for (var i = 0; i < ids.length; i++) {
|
|
859
|
+
if (overlayCards[ids[i]].key === key) dismissOverlay(ids[i]);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function clearOverlays() {
|
|
863
|
+
var ids = Object.keys(overlayCards);
|
|
864
|
+
for (var i = 0; i < ids.length; i++) dismissOverlay(ids[i]);
|
|
865
|
+
if (overlayLayer) {
|
|
866
|
+
try { if (overlayLayer.parentNode) overlayLayer.parentNode.removeChild(overlayLayer); } catch (e) {}
|
|
867
|
+
overlayLayer = null;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
function overlayCount() { return Object.keys(overlayCards).length; }
|
|
871
|
+
// Same inputs as showToast: `quick` is the approval record built by
|
|
872
|
+
// quickFor(), and it is what lets an overlay settle the request in place.
|
|
873
|
+
function showOverlay(kind, title, body, sessionId, quick) {
|
|
874
|
+
if (!config.overlay) return false;
|
|
875
|
+
var layer = ensureOverlayLayer();
|
|
876
|
+
if (!layer) return false;
|
|
877
|
+
var d = texts();
|
|
878
|
+
try {
|
|
879
|
+
var id = "ov" + (++overlaySeq);
|
|
880
|
+
var card = document.createElement("div");
|
|
881
|
+
card.className = "dnm-ov dnm-ov-" + (kind === "attention" ? "attention" : "done");
|
|
882
|
+
card.setAttribute("role", kind === "attention" ? "alert" : "status");
|
|
883
|
+
var head = document.createElement("div");
|
|
884
|
+
head.className = "dnm-ov-head";
|
|
885
|
+
var dot = document.createElement("span");
|
|
886
|
+
dot.className = "dnm-ov-dot";
|
|
887
|
+
var label = document.createElement("span");
|
|
888
|
+
label.className = "dnm-ov-title";
|
|
889
|
+
label.textContent = String(title == null ? "" : title);
|
|
890
|
+
var close = document.createElement("button");
|
|
891
|
+
close.type = "button";
|
|
892
|
+
close.className = "dnm-ov-x";
|
|
893
|
+
close.setAttribute("aria-label", d.dismiss);
|
|
894
|
+
close.textContent = "×";
|
|
895
|
+
close.onclick = function (ev) {
|
|
896
|
+
if (ev && ev.stopPropagation) ev.stopPropagation();
|
|
897
|
+
dismissOverlay(id);
|
|
898
|
+
};
|
|
899
|
+
head.appendChild(dot);
|
|
900
|
+
head.appendChild(label);
|
|
901
|
+
head.appendChild(close);
|
|
902
|
+
card.appendChild(head);
|
|
903
|
+
if (body) {
|
|
904
|
+
var bodyEl = document.createElement("div");
|
|
905
|
+
bodyEl.className = "dnm-ov-body";
|
|
906
|
+
bodyEl.textContent = String(body);
|
|
907
|
+
card.appendChild(bodyEl);
|
|
908
|
+
}
|
|
909
|
+
if (quick && typeof quick.key === "string") {
|
|
910
|
+
var actions = document.createElement("div");
|
|
911
|
+
actions.className = "dnm-ov-actions";
|
|
912
|
+
var actionBtn = function (cls, text, outcome) {
|
|
913
|
+
var b = document.createElement("button");
|
|
914
|
+
b.type = "button";
|
|
915
|
+
b.className = "dnm-ov-btn " + cls;
|
|
916
|
+
b.textContent = text;
|
|
917
|
+
b.onclick = function (ev) {
|
|
918
|
+
if (ev && ev.stopPropagation) ev.stopPropagation();
|
|
919
|
+
try { decidePending(quick.key, outcome); } catch (e) {}
|
|
920
|
+
dismissOverlay(id);
|
|
921
|
+
};
|
|
922
|
+
return b;
|
|
923
|
+
};
|
|
924
|
+
actions.appendChild(actionBtn("dnm-ov-approve", d.approve, "allowed-once"));
|
|
925
|
+
actions.appendChild(actionBtn("dnm-ov-reject", d.reject, "rejected"));
|
|
926
|
+
card.appendChild(actions);
|
|
927
|
+
}
|
|
928
|
+
// A click on the card body opens the conversation, exactly like a toast
|
|
929
|
+
// body click — and here it is an ordinary page gesture, so it reaches the
|
|
930
|
+
// desktop's dsh:// raise path even though a worker click would not.
|
|
931
|
+
card.onclick = function () {
|
|
932
|
+
if (config.autoFocus) focusSession(sessionId || null);
|
|
933
|
+
dismissOverlay(id);
|
|
934
|
+
};
|
|
935
|
+
layer.appendChild(card);
|
|
936
|
+
var rec = { card: card, key: (quick && quick.key) ? quick.key : null, kind: kind, timer: null };
|
|
937
|
+
overlayCards[id] = rec;
|
|
938
|
+
var ids = Object.keys(overlayCards);
|
|
939
|
+
for (var i = 0; i < ids.length - OVERLAY_MAX_CARDS; i++) dismissOverlay(ids[i]);
|
|
940
|
+
if (!(quick && typeof quick.key === "string")) {
|
|
941
|
+
var ttl = OVERLAY_TTL[kind === "attention" ? "attention" : "done"];
|
|
942
|
+
if (ttl) rec.timer = setTimeout(function () { dismissOverlay(id); }, ttl);
|
|
943
|
+
}
|
|
944
|
+
return true;
|
|
945
|
+
} catch (e) { return false; }
|
|
946
|
+
}
|
|
737
947
|
// Sound: WebAudio beeps (no asset needed). The context only runs after a
|
|
738
948
|
// user gesture; we unlock it on the first pointer/key event.
|
|
739
949
|
var audioCtx = null;
|
|
@@ -1044,7 +1254,10 @@
|
|
|
1044
1254
|
var rec = pendingActions[key];
|
|
1045
1255
|
if (rec) {
|
|
1046
1256
|
delete pendingActions[key];
|
|
1047
|
-
if (!rec.test)
|
|
1257
|
+
if (!rec.test) {
|
|
1258
|
+
closeNotificationTag(rec.tag);
|
|
1259
|
+
closeOverlayForKey(key);
|
|
1260
|
+
}
|
|
1048
1261
|
}
|
|
1049
1262
|
if (!attentionKeysMarked[key]) return;
|
|
1050
1263
|
delete attentionKeysMarked[key];
|
|
@@ -1084,6 +1297,7 @@
|
|
|
1084
1297
|
function deliver(kind, title, body, sessionId, quick) {
|
|
1085
1298
|
if (!config.enabled) return;
|
|
1086
1299
|
showToast(kind, title, body, sessionId, quick);
|
|
1300
|
+
showOverlay(kind, title, body, sessionId, quick);
|
|
1087
1301
|
if (kind === "attention") {
|
|
1088
1302
|
playAttentionSound();
|
|
1089
1303
|
} else {
|
|
@@ -1723,7 +1937,7 @@
|
|
|
1723
1937
|
});
|
|
1724
1938
|
} catch (e) {}
|
|
1725
1939
|
// keep in sync with package.json "version" on every release
|
|
1726
|
-
api.version = "1.
|
|
1940
|
+
api.version = "1.9.0";
|
|
1727
1941
|
api.setConfig = function (patch) { return setConfigLocal(patch); };
|
|
1728
1942
|
api.resetConfig = function () {
|
|
1729
1943
|
try {
|
|
@@ -1791,6 +2005,14 @@
|
|
|
1791
2005
|
customSoundDone: !!readSound("done"),
|
|
1792
2006
|
customSoundAttention: !!readSound("attention"),
|
|
1793
2007
|
quickActions: config.quickActions,
|
|
2008
|
+
// in-page overlay: the alert channel that follows the DSH window to
|
|
2009
|
+
// whatever display it sits on, instead of the OS toast's fixed primary
|
|
2010
|
+
// display. cards is the live stack, corner the configured anchor.
|
|
2011
|
+
overlay: {
|
|
2012
|
+
enabled: !!config.overlay,
|
|
2013
|
+
corner: overlayCorner(),
|
|
2014
|
+
cards: overlayCount()
|
|
2015
|
+
},
|
|
1794
2016
|
// external webhook forwarding: counts come from externalStat (tallied
|
|
1795
2017
|
// on every send), targets is the raw vs validated count.
|
|
1796
2018
|
external: {
|
|
@@ -1837,9 +2059,19 @@
|
|
|
1837
2059
|
var NS = "dsh-notify-me";
|
|
1838
2060
|
var UI_zh = {
|
|
1839
2061
|
nav: "通知提醒",
|
|
1840
|
-
blurb: "
|
|
2062
|
+
blurb: "模型停下来需要你操作,或回复在后台完成时提醒你。",
|
|
2063
|
+
secWays: "提醒方式",
|
|
2064
|
+
secWhen: "什么时候提醒",
|
|
2065
|
+
secTest: "试一试",
|
|
2066
|
+
advanced: "高级设置",
|
|
2067
|
+
advancedHint: "音效来源 · 外部通知 · 运行环境",
|
|
2068
|
+
secDiag: "运行环境与诊断",
|
|
2069
|
+
expand: "展开",
|
|
2070
|
+
collapse: "收起",
|
|
2071
|
+
permHintInline: "尚未授权,允许一次即可。",
|
|
2072
|
+
resetHint: "把上面所有选项还原成默认值(含已选的自定义音效)。",
|
|
1841
2073
|
enable: "启用提醒",
|
|
1842
|
-
enableHint: "
|
|
2074
|
+
enableHint: "关闭后不弹通知、不响提示音,也不改标签页标题。",
|
|
1843
2075
|
hostEnv: "运行环境",
|
|
1844
2076
|
hostEnvHint: "默认自动识别:页面协议是 dsh-app:// 即桌面端(DSH Desktop),其余为 Web 端。识别不准时手动指定:桌面端点通知借 dsh:// 深链把窗口带到前台、快捷裁决按钮不可用;Web 端走 window.focus() 与 Service Worker 桥。",
|
|
1845
2077
|
hostEnvAuto: "自动识别(推荐)",
|
|
@@ -1849,11 +2081,11 @@
|
|
|
1849
2081
|
hostEnvMismatchTail: "」,手动选择会覆盖它)",
|
|
1850
2082
|
hostEnvDesktopForcedNote: "(当前页面不是 dsh-app://:点通知会尝试唤起 dsh:// 协议,浏览器可能弹「打开应用」确认框)",
|
|
1851
2083
|
toast: "系统通知",
|
|
1852
|
-
toastHint: "
|
|
2084
|
+
toastHint: "经浏览器弹系统通知(Windows 通知中心),需要通知权限。",
|
|
1853
2085
|
sound: "提示音",
|
|
1854
|
-
soundHint: "
|
|
2086
|
+
soundHint: "提示音总开关;播什么由「高级设置 → 音效来源」决定。",
|
|
1855
2087
|
soundSource: "音效来源",
|
|
1856
|
-
soundSourceHint: "
|
|
2088
|
+
soundSourceHint: "两类提醒各自选声音来源,互不影响;默认是插件内置合成音。",
|
|
1857
2089
|
soundDoneLabel: "「回复完成」的声音",
|
|
1858
2090
|
soundAttentionLabel: "「需要你」的声音",
|
|
1859
2091
|
soundSynth: "内置合成音(默认)",
|
|
@@ -1876,7 +2108,7 @@
|
|
|
1876
2108
|
soundOffHint: "「提示音」开关是关的:先打开它才能试听(关掉时提醒本身也确实不出声)。",
|
|
1877
2109
|
soundPreviewSystem: "「系统通知音」是随通知由系统播放的,插件这边没有可播的音;已发一条测试提醒,声音就是它。",
|
|
1878
2110
|
external: "外部通知",
|
|
1879
|
-
externalHint: "
|
|
2111
|
+
externalHint: "把提醒同时推到 Discord / Bark / Gotify / 自定义 Webhook,人不在电脑前也能收到。与本地提醒同一套触发规则。",
|
|
1880
2112
|
externalEnable: "转发外部通知",
|
|
1881
2113
|
externalEnableHint: "总开关:关闭后不向任何外部端点发送。",
|
|
1882
2114
|
externalAttentionLabel: "转发「需要你」",
|
|
@@ -1907,21 +2139,30 @@
|
|
|
1907
2139
|
externalNoTargets: "先添加至少一个推送目标。",
|
|
1908
2140
|
externalOffHint: "先打开「转发外部通知」。",
|
|
1909
2141
|
attentionVisible: "页面打开时也提醒「需要你」",
|
|
1910
|
-
attentionVisibleHint: "
|
|
2142
|
+
attentionVisibleHint: "页面在最前也提醒。关掉后只有切到后台才提醒。",
|
|
1911
2143
|
currentQuiet: "当前对话不弹通知",
|
|
1912
|
-
currentQuietHint: "
|
|
2144
|
+
currentQuietHint: "等待就发生在你正看着的对话里时只留标签页标记,免得挡住审批卡片;切到后台后照常提醒。",
|
|
1913
2145
|
doneVisible: "页面打开时也提醒「回复完成」",
|
|
1914
|
-
doneVisibleHint: "
|
|
2146
|
+
doneVisibleHint: "页面在最前也提醒。默认只在切到后台时提醒,避免打扰。",
|
|
1915
2147
|
subagentQuiet: "子代理会话不提醒",
|
|
1916
|
-
subagentQuietHint: "
|
|
2148
|
+
subagentQuietHint: "跳过后台子代理会话(结果由所属主对话的提醒覆盖);你正打开的那个照常提醒。",
|
|
1917
2149
|
autoFocus: "点击通知回到对应对话",
|
|
1918
|
-
autoFocusHint: "
|
|
2150
|
+
autoFocusHint: "点通知会切到这条提醒所属的对话;关掉后点击不做任何事。",
|
|
1919
2151
|
quickActions: "审批通知上直接裁决",
|
|
1920
|
-
quickActionsHint: "
|
|
2152
|
+
quickActionsHint: "审批通知上带「同意 / 拒绝」,点一下即完成裁决,不用切回页面。需要 Service Worker(http://127.0.0.1 或 https),用不了时退回普通通知。",
|
|
1921
2153
|
quickApprovalNote: "「同意」等价于审批卡片上的「允许一次」,只放行这一次。",
|
|
1922
2154
|
bridgeOff: "快捷裁决按钮暂不可用(",
|
|
1923
2155
|
bridgeOffTail: "):审批通知会退回不带按钮的普通样式。",
|
|
1924
2156
|
bridgeOffDesktop: "桌面端暂不支持快捷裁决按钮:dsh-app:// 页面无法注册 Service Worker,审批通知只能是不带按钮的普通样式,测试按钮同样只发普通通知。这是宿主限制,不是设置坏了。",
|
|
2157
|
+
overlay: "页面浮层提醒",
|
|
2158
|
+
overlayHint: "在页面角落画提醒卡片:窗口在哪块屏,它就在哪块屏。系统通知只会出现在主屏,主屏不看时用它。不需要通知权限,桌面端也能直接「同意 / 拒绝」,可与系统通知同开。",
|
|
2159
|
+
overlayCorner: "浮层位置",
|
|
2160
|
+
overlayCornerHint: "贴在 DSH 窗口的哪个角;窗口在哪块屏,浮层就出现在哪块屏。",
|
|
2161
|
+
overlayPosTR: "右上",
|
|
2162
|
+
overlayPosBR: "右下",
|
|
2163
|
+
overlayPosTL: "左上",
|
|
2164
|
+
overlayPosBL: "左下",
|
|
2165
|
+
overlayQuickNote: "浮层上的「同意」同样等价于审批卡片上的「允许一次」,与通知按钮一致。",
|
|
1925
2166
|
testApprovalNoBridge: "已发普通测试通知(快捷裁决按钮需要 Service Worker,当前环境没有,按钮渲染不了)。",
|
|
1926
2167
|
testNeedsQuick: "先打开上面的「审批通知上直接裁决」才能测按钮。",
|
|
1927
2168
|
volume: "音量",
|
|
@@ -1932,7 +2173,7 @@
|
|
|
1932
2173
|
langAutoNote: "(跟随界面)",
|
|
1933
2174
|
current: "界面当前语言",
|
|
1934
2175
|
testTitle: "快速测试",
|
|
1935
|
-
testHint: "
|
|
2176
|
+
testHint: "用当前设置各发一条,不受「什么时候提醒」里的开关限制。",
|
|
1936
2177
|
testAttention: "测试「需要你」",
|
|
1937
2178
|
testDone: "测试「回复完成」",
|
|
1938
2179
|
testApprovalBtn: "测试「审批按钮」",
|
|
@@ -1945,9 +2186,19 @@
|
|
|
1945
2186
|
};
|
|
1946
2187
|
var UI_en = {
|
|
1947
2188
|
nav: "Notify me",
|
|
1948
|
-
blurb: "
|
|
2189
|
+
blurb: "Get told when the agent needs your input, or when a reply finishes in the background.",
|
|
2190
|
+
secWays: "How you get told",
|
|
2191
|
+
secWhen: "When to alert",
|
|
2192
|
+
secTest: "Try it",
|
|
2193
|
+
advanced: "Advanced",
|
|
2194
|
+
advancedHint: "Alert sounds · External forwarding · Runtime",
|
|
2195
|
+
secDiag: "Runtime & diagnostics",
|
|
2196
|
+
expand: "Show",
|
|
2197
|
+
collapse: "Hide",
|
|
2198
|
+
permHintInline: "Not granted yet — allow it once from the page.",
|
|
2199
|
+
resetHint: "Put every option above back to its default (picked sound files included).",
|
|
1949
2200
|
enable: "Enable reminders",
|
|
1950
|
-
enableHint: "
|
|
2201
|
+
enableHint: "When off: no toasts, no sounds, and the tab title is left alone.",
|
|
1951
2202
|
hostEnv: "Environment",
|
|
1952
2203
|
hostEnvHint: "Auto-detect by default: a dsh-app:// page is the Desktop (DSH Desktop) shell, anything else is the Web. Override it if detection comes out wrong: on the Desktop a toast click raises the window through the dsh:// deep link and quick-decision buttons are unavailable; on the Web it is window.focus() plus the Service Worker bridge.",
|
|
1953
2204
|
hostEnvAuto: "Auto-detect (recommended)",
|
|
@@ -1957,11 +2208,11 @@
|
|
|
1957
2208
|
hostEnvMismatchTail: "\"; the manual choice overrides it)",
|
|
1958
2209
|
hostEnvDesktopForcedNote: " (this page is not dsh-app://: a toast click will try to launch the dsh:// protocol and the browser may show an \"open app?\" prompt)",
|
|
1959
2210
|
toast: "System notifications",
|
|
1960
|
-
toastHint: "Native toasts
|
|
2211
|
+
toastHint: "Native toasts through the browser (Windows notification center); needs notification permission.",
|
|
1961
2212
|
sound: "Sound",
|
|
1962
|
-
soundHint: "Master cue switch; what it
|
|
2213
|
+
soundHint: "Master cue switch; what it plays comes from Advanced → Alert sound.",
|
|
1963
2214
|
soundSource: "Alert sound",
|
|
1964
|
-
soundSourceHint: "Each alert kind picks its own
|
|
2215
|
+
soundSourceHint: "Each alert kind picks its own source; the two never affect each other. Default is the built-in cue.",
|
|
1965
2216
|
soundDoneLabel: "\"Reply finished\" sound",
|
|
1966
2217
|
soundAttentionLabel: "\"Needs you\" sound",
|
|
1967
2218
|
soundSynth: "Built-in cue (default)",
|
|
@@ -1984,7 +2235,7 @@
|
|
|
1984
2235
|
soundOffHint: "The Sound switch is off: turn it on to preview (with it off the alerts themselves stay silent too).",
|
|
1985
2236
|
soundPreviewSystem: "The system sound plays with a toast, not from the plugin — a test alert was sent instead; that sound is it.",
|
|
1986
2237
|
external: "External notifications",
|
|
1987
|
-
externalHint: "
|
|
2238
|
+
externalHint: "Also push alerts to Discord / Bark / Gotify / custom webhooks, so you hear about it away from the machine. Same trigger rules as local alerts.",
|
|
1988
2239
|
externalEnable: "Forward to external targets",
|
|
1989
2240
|
externalEnableHint: "Master switch: when off, nothing is sent to any external endpoint.",
|
|
1990
2241
|
externalAttentionLabel: "Forward \"needs you\"",
|
|
@@ -2015,21 +2266,30 @@
|
|
|
2015
2266
|
externalNoTargets: "Add at least one push target first.",
|
|
2016
2267
|
externalOffHint: "Turn on \"Forward to external targets\" first.",
|
|
2017
2268
|
attentionVisible: "Also alert \"needs you\" while the page is open",
|
|
2018
|
-
attentionVisibleHint: "
|
|
2269
|
+
attentionVisibleHint: "Alert even while the page is in front. Turn off to hear only from the background.",
|
|
2019
2270
|
currentQuiet: "Stay quiet for the conversation on screen",
|
|
2020
|
-
currentQuietHint: "
|
|
2271
|
+
currentQuietHint: "A wait belonging to the conversation on screen keeps only the tab marker, so the approval card stays readable; backgrounding restores alerts.",
|
|
2021
2272
|
doneVisible: "Also alert \"reply finished\" while the page is open",
|
|
2022
|
-
doneVisibleHint: "
|
|
2273
|
+
doneVisibleHint: "Alert even while the page is in front. By default only background finishes alert.",
|
|
2023
2274
|
subagentQuiet: "Ignore subagent sessions",
|
|
2024
|
-
subagentQuietHint: "
|
|
2275
|
+
subagentQuietHint: "Skip sessions spawned by a subagent (the parent conversation covers them); the one you have open still alerts.",
|
|
2025
2276
|
autoFocus: "Click a toast to open that conversation",
|
|
2026
|
-
autoFocusHint: "
|
|
2277
|
+
autoFocusHint: "A click switches to the conversation the alert came from; off means a click does nothing.",
|
|
2027
2278
|
quickActions: "Decide approvals from the toast",
|
|
2028
|
-
quickActionsHint: "
|
|
2279
|
+
quickActionsHint: "Approval notifications carry Approve / Reject; one click settles the request without switching back. Needs a Service Worker browser (http://127.0.0.1 or https), else plain toasts.",
|
|
2029
2280
|
quickApprovalNote: "\"Approve\" equals \"Allow once\" on the approval card — it allows that one call only.",
|
|
2030
2281
|
bridgeOff: "Quick-decision buttons are unavailable right now (",
|
|
2031
2282
|
bridgeOffTail: "); approval toasts fall back to their plain form.",
|
|
2032
2283
|
bridgeOffDesktop: "Quick-decision buttons are unavailable on Desktop: dsh-app:// pages cannot register a Service Worker, so approval toasts fall back to their plain, button-less form and the test button only sends a plain notification. This is a host limitation, not a broken setting.",
|
|
2284
|
+
overlay: "In-page overlay",
|
|
2285
|
+
overlayHint: "Draw a card in the page corner, on whatever display the window sits on. System notifications only land on the primary display. No permission needed, and the Desktop shell can Approve / Reject right on the card. Can run with toasts on.",
|
|
2286
|
+
overlayCorner: "Overlay position",
|
|
2287
|
+
overlayCornerHint: "Which corner of the DSH window the cards stack in; they follow the window to whatever display it is on.",
|
|
2288
|
+
overlayPosTR: "Top right",
|
|
2289
|
+
overlayPosBR: "Bottom right",
|
|
2290
|
+
overlayPosTL: "Top left",
|
|
2291
|
+
overlayPosBL: "Bottom left",
|
|
2292
|
+
overlayQuickNote: "\"Approve\" here equals \"Allow once\" on the approval card — the same decision the toast buttons send.",
|
|
2033
2293
|
testApprovalNoBridge: "Plain test notification sent (quick-decision buttons need a Service Worker, which this environment lacks, so they cannot render).",
|
|
2034
2294
|
testNeedsQuick: "Turn on \"Decide approvals from the toast\" above to test the buttons.",
|
|
2035
2295
|
volume: "Volume",
|
|
@@ -2040,7 +2300,7 @@
|
|
|
2040
2300
|
langAutoNote: " (follows the interface)",
|
|
2041
2301
|
current: "Current interface language",
|
|
2042
2302
|
testTitle: "Quick test",
|
|
2043
|
-
testHint: "Send one
|
|
2303
|
+
testHint: "Send one of each with the current settings; the \"When to alert\" switches are bypassed.",
|
|
2044
2304
|
testAttention: "Test \"needs you\"",
|
|
2045
2305
|
testDone: "Test \"reply finished\"",
|
|
2046
2306
|
testApprovalBtn: "Test approval buttons",
|
|
@@ -2082,7 +2342,27 @@
|
|
|
2082
2342
|
".dnm-targetRow+.dnm-targetRow{border-top:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.35))}" +
|
|
2083
2343
|
".dnm-input{flex:1;min-width:180px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4));background:var(--dsw-alias-bg-layer-1,rgba(255,255,255,.03));color:var(--dsw-alias-label-primary,rgba(20,20,20,.9));border-radius:8px;padding:5px 8px;font-size:12px;font-family:inherit}" +
|
|
2084
2344
|
".dnm-hint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,rgba(120,120,120,.9))}" +
|
|
2085
|
-
".dnm-feedback{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,rgba(120,120,120,.9));min-height:18px}"
|
|
2345
|
+
".dnm-feedback{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,rgba(120,120,120,.9));min-height:18px}" +
|
|
2346
|
+
// Page header: mark + title + one-line purpose. The section titles below
|
|
2347
|
+
// carry the grouping, so the header stays small.
|
|
2348
|
+
".dnm-head{display:flex;align-items:flex-start;gap:12px}" +
|
|
2349
|
+
".dnm-headIcon{flex:none;width:36px;height:36px;border-radius:10px;display:grid;place-items:center;color:var(--dsw-alias-state-business-primary,#3b82f6);background:rgba(59,130,246,.12);border:1px solid rgba(59,130,246,.24)}" +
|
|
2350
|
+
".dnm-headText{min-width:0;display:flex;flex-direction:column;gap:3px;padding-top:1px}" +
|
|
2351
|
+
".dnm-headTitle{font-size:15px;font-weight:600;line-height:21px;color:var(--dsw-alias-label-primary,rgba(20,20,20,.9))}" +
|
|
2352
|
+
".dnm-headSub{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,rgba(120,120,120,.9))}" +
|
|
2353
|
+
".dnm-secTitle{margin:2px 0 -6px;padding:0 2px;font-size:12px;font-weight:600;line-height:18px;color:var(--dsw-alias-label-secondary,rgba(90,90,90,.9));letter-spacing:.02em}" +
|
|
2354
|
+
// Sub-rows hang under the row they belong to (corner of the overlay,
|
|
2355
|
+
// volume under Sound) instead of reading as siblings.
|
|
2356
|
+
".dnm-rowSub{padding-left:26px;background:var(--dsw-alias-bg-layer-2,rgba(128,128,128,.04))}" +
|
|
2357
|
+
".dnm-rowSub .dnm-rowLabel{font-weight:500}" +
|
|
2358
|
+
".dnm-rowStack{flex-direction:column;align-items:stretch;gap:10px}" +
|
|
2359
|
+
".dnm-rowStack .dnm-btnRow{justify-content:flex-start;flex-wrap:wrap}" +
|
|
2360
|
+
".dnm-advToggle{width:100%;box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:10px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.35));background:transparent;color:var(--dsw-alias-label-primary,rgba(20,20,20,.9));border-radius:10px;padding:10px 14px;font-family:inherit;font-size:13px;font-weight:600;cursor:pointer;text-align:left}" +
|
|
2361
|
+
".dnm-advToggle:hover{background:var(--dsw-alias-bg-layer-1,rgba(128,128,128,.08))}" +
|
|
2362
|
+
".dnm-advToggle:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#3b82f6);outline-offset:2px}" +
|
|
2363
|
+
".dnm-advRight{display:flex;align-items:center;gap:8px;flex:none;font-weight:400;color:var(--dsw-alias-label-tertiary,rgba(120,120,120,.9))}" +
|
|
2364
|
+
".dnm-advChev{font-size:10px;translate:0 1px;transition:transform .15s ease}" +
|
|
2365
|
+
".dnm-advChev.open{transform:rotate(180deg)}";
|
|
2086
2366
|
(function injectStyle() {
|
|
2087
2367
|
try {
|
|
2088
2368
|
if (typeof document === "undefined" || typeof document.createElement !== "function") return;
|
|
@@ -2366,43 +2646,75 @@
|
|
|
2366
2646
|
else if (cfg.hostEnv !== "auto" && isDesktopShell() !== detectedDesktop()) {
|
|
2367
2647
|
envHint += t("hostEnvMismatch") + (detectedDesktop() ? t("hostEnvDesktop") : t("hostEnvWeb")) + t("hostEnvMismatchTail");
|
|
2368
2648
|
}
|
|
2649
|
+
// Page header: the plugin's own mark instead of the host's generic
|
|
2650
|
+
// settings gear, so the page is recognisable at a glance while the
|
|
2651
|
+
// Settings nav row itself stays host-controlled.
|
|
2652
|
+
function headIcon() {
|
|
2653
|
+
return h("svg", { viewBox: "0 0 24 24", width: 22, height: 22, fill: "none", stroke: "currentColor", "stroke-width": 1.6, "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true" }, [
|
|
2654
|
+
h("path", { d: "M12 3.4a5.1 5.1 0 0 0-5.1 5.1v3.1c0 .9-.3 1.7-.9 2.4l-1.1 1.3c-.5.6-.1 1.5.7 1.5h12.8c.8 0 1.2-.9.7-1.5l-1.1-1.3a3.8 3.8 0 0 1-.9-2.4V8.5A5.1 5.1 0 0 0 12 3.4Z" }),
|
|
2655
|
+
h("path", { d: "M9.8 19.2a2.4 2.4 0 0 0 4.4 0" }),
|
|
2656
|
+
h("circle", { cx: 18.3, cy: 5.3, r: 2.35, fill: "currentColor", stroke: "none" })
|
|
2657
|
+
]);
|
|
2658
|
+
}
|
|
2659
|
+
function secTitle(labelText) {
|
|
2660
|
+
return h("div", { className: "dnm-secTitle" }, labelText);
|
|
2661
|
+
}
|
|
2662
|
+
// The permission state belongs on the System-notifications row it affects
|
|
2663
|
+
// rather than in a footer note the user has to connect to it.
|
|
2664
|
+
function toastHintText() {
|
|
2665
|
+
return t("toastHint") + (permOK ? "" : " " + t("permHintInline"));
|
|
2666
|
+
}
|
|
2667
|
+
var advState = React.useState(false);
|
|
2668
|
+
var advancedOpen = advState[0];
|
|
2669
|
+
var setAdvancedOpen = advState[1];
|
|
2670
|
+
var testRow = h("div", { className: "dnm-row dnm-rowStack" }, [
|
|
2671
|
+
h("div", { className: "dnm-rowText" }, [
|
|
2672
|
+
h("div", { className: "dnm-rowLabel" }, t("testTitle")),
|
|
2673
|
+
h("div", { className: "dnm-rowHint" }, t("testHint"))
|
|
2674
|
+
]),
|
|
2675
|
+
h("div", { className: "dnm-btnRow" }, [
|
|
2676
|
+
h("button", { type: "button", className: "dnm-btn", disabled: !cfg.enabled, onClick: function () { runTest("attention"); } }, t("testAttention")),
|
|
2677
|
+
h("button", { type: "button", className: "dnm-btn", disabled: !cfg.enabled, onClick: function () { runTest("done"); } }, t("testDone")),
|
|
2678
|
+
h("button", { type: "button", className: "dnm-btn", disabled: !cfg.enabled || !cfg.quickActions, onClick: function () { runTest("approval"); } }, t("testApprovalBtn"))
|
|
2679
|
+
])
|
|
2680
|
+
]);
|
|
2369
2681
|
return h("div", { className: "dnm-wrap" }, [
|
|
2370
|
-
h("
|
|
2682
|
+
h("div", { className: "dnm-head" }, [
|
|
2683
|
+
h("span", { className: "dnm-headIcon" }, headIcon()),
|
|
2684
|
+
h("div", { className: "dnm-headText" }, [
|
|
2685
|
+
h("div", { className: "dnm-headTitle" }, t("nav")),
|
|
2686
|
+
h("p", { className: "dnm-headSub" }, t("blurb"))
|
|
2687
|
+
])
|
|
2688
|
+
]),
|
|
2689
|
+
// 1) How the alert reaches you. Each channel owns its own row, and the
|
|
2690
|
+
// row's dependent control (overlay corner, volume) hangs beneath it.
|
|
2691
|
+
secTitle(t("secWays")),
|
|
2371
2692
|
card([
|
|
2372
2693
|
rowSwitch("enabled", t("enable"), t("enableHint"), cfg.enabled, function (v) {
|
|
2373
2694
|
patch({ enabled: v });
|
|
2374
2695
|
if (!v && attentionActive > 0) { attentionActive = 0; applyTitle(); }
|
|
2375
2696
|
}),
|
|
2376
|
-
|
|
2697
|
+
rowSwitch("toast", t("toast"), toastHintText(), cfg.toast, function (v) { patch({ toast: v }); }),
|
|
2698
|
+
rowSwitch("overlay", t("overlay"), t("overlayHint"), cfg.overlay, function (v) { patch({ overlay: v }); }),
|
|
2699
|
+
cfg.overlay ? h("div", { className: "dnm-row dnm-rowSub" }, [
|
|
2377
2700
|
h("div", { className: "dnm-rowText" }, [
|
|
2378
|
-
h("div", { className: "dnm-rowLabel" }, t("
|
|
2379
|
-
h("div", { className: "dnm-rowHint" },
|
|
2701
|
+
h("div", { className: "dnm-rowLabel" }, t("overlayCorner")),
|
|
2702
|
+
h("div", { className: "dnm-rowHint" }, t("overlayCornerHint"))
|
|
2380
2703
|
]),
|
|
2381
2704
|
h("select", {
|
|
2382
2705
|
className: "dnm-select",
|
|
2383
|
-
value:
|
|
2384
|
-
"aria-label": t("
|
|
2385
|
-
onChange: function (e) { patch({
|
|
2706
|
+
value: overlayCorner(),
|
|
2707
|
+
"aria-label": t("overlayCorner"),
|
|
2708
|
+
onChange: function (e) { patch({ overlayCorner: e.currentTarget.value }); }
|
|
2386
2709
|
}, [
|
|
2387
|
-
h("option", { value: "
|
|
2388
|
-
h("option", { value: "
|
|
2389
|
-
h("option", { value: "
|
|
2710
|
+
h("option", { value: "top-right" }, t("overlayPosTR")),
|
|
2711
|
+
h("option", { value: "bottom-right" }, t("overlayPosBR")),
|
|
2712
|
+
h("option", { value: "top-left" }, t("overlayPosTL")),
|
|
2713
|
+
h("option", { value: "bottom-left" }, t("overlayPosBL"))
|
|
2390
2714
|
])
|
|
2391
|
-
])
|
|
2392
|
-
]),
|
|
2393
|
-
card([
|
|
2394
|
-
rowSwitch("toast", t("toast"), t("toastHint"), cfg.toast, function (v) { patch({ toast: v }); }),
|
|
2715
|
+
]) : null,
|
|
2395
2716
|
rowSwitch("sound", t("sound"), t("soundHint"), cfg.sound, function (v) { patch({ sound: v }); }),
|
|
2396
|
-
|
|
2397
|
-
rowSwitch("currentQuiet", t("currentQuiet"), t("currentQuietHint"), cfg.currentHiddenOnly, function (v) { patch({ currentHiddenOnly: v }); }),
|
|
2398
|
-
rowSwitch("doneVisible", t("doneVisible"), t("doneVisibleHint"), !cfg.doneHiddenOnly, function (v) { patch({ doneHiddenOnly: !v }); }),
|
|
2399
|
-
rowSwitch("subagentQuiet", t("subagentQuiet"), t("subagentQuietHint"), cfg.ignoreSubagent, function (v) { patch({ ignoreSubagent: v }); }),
|
|
2400
|
-
rowSwitch("autoFocus", t("autoFocus"), t("autoFocusHint"), cfg.autoFocus, function (v) { patch({ autoFocus: v }); }),
|
|
2401
|
-
rowSwitch("quickActions", t("quickActions"), t("quickActionsHint"), cfg.quickActions, function (v) {
|
|
2402
|
-
patch({ quickActions: v });
|
|
2403
|
-
if (v) ensureActionBridge();
|
|
2404
|
-
}),
|
|
2405
|
-
h("div", { className: "dnm-row" }, [
|
|
2717
|
+
cfg.sound ? h("div", { className: "dnm-row dnm-rowSub" }, [
|
|
2406
2718
|
h("div", { className: "dnm-rowText" }, [
|
|
2407
2719
|
h("div", { className: "dnm-rowLabel" }, t("volume")),
|
|
2408
2720
|
h("div", { className: "dnm-rowHint" }, String(Math.round(cfg.volume * 100)) + "%")
|
|
@@ -2415,126 +2727,159 @@
|
|
|
2415
2727
|
"aria-label": t("volume"),
|
|
2416
2728
|
onChange: function (e) { patch({ volume: Number(e.currentTarget.value) / 100 }); }
|
|
2417
2729
|
})
|
|
2418
|
-
])
|
|
2730
|
+
]) : null,
|
|
2731
|
+
rowSwitch("quickActions", t("quickActions"), t("quickActionsHint"), cfg.quickActions, function (v) {
|
|
2732
|
+
patch({ quickActions: v });
|
|
2733
|
+
if (v) ensureActionBridge();
|
|
2734
|
+
})
|
|
2419
2735
|
]),
|
|
2420
|
-
//
|
|
2421
|
-
|
|
2422
|
-
// sound-related knobs read together, and re-read through soundBump
|
|
2423
|
-
// because picked files live outside the config object.
|
|
2424
|
-
card([].concat(
|
|
2425
|
-
[h("div", { className: "dnm-row" }, [
|
|
2426
|
-
h("div", { className: "dnm-rowText" }, [
|
|
2427
|
-
h("div", { className: "dnm-rowLabel" }, t("soundSource")),
|
|
2428
|
-
h("div", { className: "dnm-rowHint" }, t("soundSourceHint"))
|
|
2429
|
-
])
|
|
2430
|
-
])],
|
|
2431
|
-
soundRows("done", t("soundDoneLabel")),
|
|
2432
|
-
soundRows("attention", t("soundAttentionLabel"))
|
|
2433
|
-
)),
|
|
2434
|
-
// External forwarding: the same alerts, mirrored to the user's own
|
|
2435
|
-
// webhook endpoints so "come back" reaches them away from the machine.
|
|
2436
|
-
card([].concat(
|
|
2437
|
-
[h("div", { className: "dnm-row" }, [
|
|
2438
|
-
h("div", { className: "dnm-rowText" }, [
|
|
2439
|
-
h("div", { className: "dnm-rowLabel" }, t("external")),
|
|
2440
|
-
h("div", { className: "dnm-rowHint" }, t("externalHint"))
|
|
2441
|
-
])
|
|
2442
|
-
]),
|
|
2443
|
-
rowSwitch("externalEnabled", t("externalEnable"), t("externalEnableHint"), cfg.externalEnabled, function (v) { patch({ externalEnabled: v }); }),
|
|
2444
|
-
rowSwitch("externalAttention", t("externalAttentionLabel"), t("externalAttentionHint"), cfg.externalAttention, function (v) { patch({ externalAttention: v }); }),
|
|
2445
|
-
rowSwitch("externalDone", t("externalDoneLabel"), t("externalDoneHint"), cfg.externalDone, function (v) { patch({ externalDone: v }); }),
|
|
2446
|
-
h("div", { className: "dnm-row" }, [
|
|
2447
|
-
h("div", { className: "dnm-rowText" }, [
|
|
2448
|
-
h("div", { className: "dnm-rowLabel" }, t("externalTargetsLabel")),
|
|
2449
|
-
h("div", { className: "dnm-rowHint" }, t("externalTargetsHint"))
|
|
2450
|
-
])
|
|
2451
|
-
])],
|
|
2452
|
-
externalRows(),
|
|
2453
|
-
[(Array.isArray(cfg.externalTargets) && cfg.externalTargets.length) ? h("div", { className: "dnm-row" }, [
|
|
2454
|
-
h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-hint" }, t("externalPrivacy"))])
|
|
2455
|
-
]) : h("div", { className: "dnm-row" }, [
|
|
2456
|
-
h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-hint" }, t("externalEmpty"))])
|
|
2457
|
-
]),
|
|
2458
|
-
h("div", { className: "dnm-row" }, [
|
|
2459
|
-
h("div", { className: "dnm-btnRow" }, [
|
|
2460
|
-
h("button", {
|
|
2461
|
-
type: "button",
|
|
2462
|
-
className: "dnm-btn",
|
|
2463
|
-
"aria-label": t("externalAdd"),
|
|
2464
|
-
onClick: addTarget
|
|
2465
|
-
}, t("externalAdd"))
|
|
2466
|
-
])
|
|
2467
|
-
]),
|
|
2468
|
-
h("div", { className: "dnm-row" }, [
|
|
2469
|
-
h("div", { className: "dnm-rowText" }, [
|
|
2470
|
-
h("div", { className: "dnm-rowLabel" }, t("externalTestLabel")),
|
|
2471
|
-
h("div", { className: "dnm-rowHint" }, t("externalTestHint"))
|
|
2472
|
-
]),
|
|
2473
|
-
h("div", { className: "dnm-btnRow" }, [
|
|
2474
|
-
h("button", {
|
|
2475
|
-
type: "button",
|
|
2476
|
-
className: "dnm-btn",
|
|
2477
|
-
"aria-label": t("externalTestLabel"),
|
|
2478
|
-
disabled: !cfg.enabled,
|
|
2479
|
-
onClick: runExternalTest
|
|
2480
|
-
}, t("externalTestBtn"))
|
|
2481
|
-
])
|
|
2482
|
-
])
|
|
2483
|
-
])),
|
|
2736
|
+
// 2) Which events are worth interrupting for.
|
|
2737
|
+
secTitle(t("secWhen")),
|
|
2484
2738
|
card([
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
h("option", { value: "zh" }, t("langZh")),
|
|
2498
|
-
h("option", { value: "en" }, t("langEn"))
|
|
2499
|
-
])
|
|
2500
|
-
]),
|
|
2501
|
-
h("div", { className: "dnm-row" }, [
|
|
2502
|
-
h("div", { className: "dnm-rowText" }, [
|
|
2503
|
-
h("div", { className: "dnm-rowLabel" }, t("testTitle")),
|
|
2504
|
-
h("div", { className: "dnm-rowHint" }, t("testHint"))
|
|
2505
|
-
]),
|
|
2506
|
-
h("div", { className: "dnm-btnRow" }, [
|
|
2507
|
-
h("button", { type: "button", className: "dnm-btn", disabled: !cfg.enabled, onClick: function () { runTest("attention"); } }, t("testAttention")),
|
|
2508
|
-
h("button", { type: "button", className: "dnm-btn", disabled: !cfg.enabled, onClick: function () { runTest("done"); } }, t("testDone")),
|
|
2509
|
-
h("button", { type: "button", className: "dnm-btn", disabled: !cfg.enabled || !cfg.quickActions, onClick: function () { runTest("approval"); } }, t("testApprovalBtn"))
|
|
2510
|
-
])
|
|
2511
|
-
]),
|
|
2512
|
-
cfg.quickActions && cfg.toast && !bridgeActive() ? h("div", { className: "dnm-row" }, [
|
|
2513
|
-
h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, isDesktopShell() ? t("bridgeOffDesktop") : t("bridgeOff") + swState + t("bridgeOffTail"))])
|
|
2739
|
+
rowSwitch("attentionVisible", t("attentionVisible"), t("attentionVisibleHint"), !cfg.attentionHiddenOnly, function (v) { patch({ attentionHiddenOnly: !v }); }),
|
|
2740
|
+
rowSwitch("currentQuiet", t("currentQuiet"), t("currentQuietHint"), cfg.currentHiddenOnly, function (v) { patch({ currentHiddenOnly: v }); }),
|
|
2741
|
+
rowSwitch("doneVisible", t("doneVisible"), t("doneVisibleHint"), !cfg.doneHiddenOnly, function (v) { patch({ doneHiddenOnly: !v }); }),
|
|
2742
|
+
rowSwitch("subagentQuiet", t("subagentQuiet"), t("subagentQuietHint"), cfg.ignoreSubagent, function (v) { patch({ ignoreSubagent: v }); }),
|
|
2743
|
+
rowSwitch("autoFocus", t("autoFocus"), t("autoFocusHint"), cfg.autoFocus, function (v) { patch({ autoFocus: v }); })
|
|
2744
|
+
]),
|
|
2745
|
+
// 3) Prove the setup works without leaving the page.
|
|
2746
|
+
secTitle(t("secTest")),
|
|
2747
|
+
card([
|
|
2748
|
+
testRow,
|
|
2749
|
+
!cfg.quickActions ? h("div", { className: "dnm-row" }, [
|
|
2750
|
+
h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, t("testNeedsQuick"))])
|
|
2514
2751
|
]) : h("div", { className: "dnm-row" }, [
|
|
2515
|
-
h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, cfg.
|
|
2752
|
+
h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, cfg.overlay ? t("overlayQuickNote") : (cfg.toast && !bridgeActive() ? (isDesktopShell() ? t("bridgeOffDesktop") : t("bridgeOff") + swState + t("bridgeOffTail")) : t("quickApprovalNote")))])
|
|
2516
2753
|
]),
|
|
2517
|
-
h("div", { className: "dnm-row" }, [
|
|
2754
|
+
feedback ? h("div", { className: "dnm-row" }, [
|
|
2518
2755
|
h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-feedback" }, feedback)])
|
|
2519
|
-
]),
|
|
2520
|
-
h("div", { className: "dnm-row" }, [
|
|
2521
|
-
h("div", { className: "dnm-rowText" }, [
|
|
2522
|
-
h("div", { className: "dnm-rowLabel" }, t("reset")),
|
|
2523
|
-
h("div", { className: "dnm-rowHint" }, " ")
|
|
2524
|
-
]),
|
|
2525
|
-
h("button", { type: "button", className: "dnm-btn", onClick: function () {
|
|
2526
|
-
// api.resetConfig() also drops the picked cue files, which are
|
|
2527
|
-
// stored outside the config.
|
|
2528
|
-
setCfg(api.resetConfig());
|
|
2529
|
-
applyTitle();
|
|
2530
|
-
setSoundBump(soundBump + 1);
|
|
2531
|
-
setFeedback(t("resetDone"));
|
|
2532
|
-
} }, t("reset"))
|
|
2533
|
-
]),
|
|
2534
|
-
!permOK ? h("div", { className: "dnm-row" }, [
|
|
2535
|
-
h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, t("permHint"))])
|
|
2536
2756
|
]) : null
|
|
2537
2757
|
]),
|
|
2758
|
+
// 4) Everything most users never touch, collapsed by default. It all
|
|
2759
|
+
// stays mounted-on-demand (no hidden DOM work while closed).
|
|
2760
|
+
h("button", {
|
|
2761
|
+
type: "button",
|
|
2762
|
+
className: "dnm-advToggle",
|
|
2763
|
+
"aria-expanded": advancedOpen ? "true" : "false",
|
|
2764
|
+
"aria-label": t("advanced"),
|
|
2765
|
+
onClick: function () { setAdvancedOpen(!advancedOpen); }
|
|
2766
|
+
}, [
|
|
2767
|
+
h("span", null, t("advanced")),
|
|
2768
|
+
h("span", { className: "dnm-advRight" }, [
|
|
2769
|
+
h("span", null, advancedOpen ? t("collapse") : t("advancedHint")),
|
|
2770
|
+
h("span", { className: "dnm-advChev" + (advancedOpen ? " open" : "") }, "▼")
|
|
2771
|
+
])
|
|
2772
|
+
]),
|
|
2773
|
+
advancedOpen ? [
|
|
2774
|
+
secTitle(t("soundSource")),
|
|
2775
|
+
// Alert sound source: built-in cue, a picked file, or the platform's
|
|
2776
|
+
// own sound. Re-read through soundBump because picked files live
|
|
2777
|
+
// outside the config object.
|
|
2778
|
+
card([].concat(
|
|
2779
|
+
[h("div", { className: "dnm-row" }, [
|
|
2780
|
+
h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-hint" }, t("soundSourceHint"))])
|
|
2781
|
+
])],
|
|
2782
|
+
soundRows("done", t("soundDoneLabel")),
|
|
2783
|
+
soundRows("attention", t("soundAttentionLabel"))
|
|
2784
|
+
)),
|
|
2785
|
+
secTitle(t("external")),
|
|
2786
|
+
// External forwarding: the same alerts mirrored to the user's own
|
|
2787
|
+
// webhook endpoints so "come back" reaches them away from the machine.
|
|
2788
|
+
card([].concat(
|
|
2789
|
+
[h("div", { className: "dnm-row" }, [
|
|
2790
|
+
h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-hint" }, t("externalHint"))])
|
|
2791
|
+
])],
|
|
2792
|
+
[rowSwitch("externalEnabled", t("externalEnable"), t("externalEnableHint"), cfg.externalEnabled, function (v) { patch({ externalEnabled: v }); })],
|
|
2793
|
+
[rowSwitch("externalAttention", t("externalAttentionLabel"), t("externalAttentionHint"), cfg.externalAttention, function (v) { patch({ externalAttention: v }); })],
|
|
2794
|
+
[rowSwitch("externalDone", t("externalDoneLabel"), t("externalDoneHint"), cfg.externalDone, function (v) { patch({ externalDone: v }); })],
|
|
2795
|
+
[h("div", { className: "dnm-row" }, [
|
|
2796
|
+
h("div", { className: "dnm-rowText" }, [
|
|
2797
|
+
h("div", { className: "dnm-rowLabel" }, t("externalTargetsLabel")),
|
|
2798
|
+
h("div", { className: "dnm-rowHint" }, t("externalTargetsHint"))
|
|
2799
|
+
])
|
|
2800
|
+
])],
|
|
2801
|
+
externalRows(),
|
|
2802
|
+
[(Array.isArray(cfg.externalTargets) && cfg.externalTargets.length) ? h("div", { className: "dnm-row" }, [
|
|
2803
|
+
h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-hint" }, t("externalPrivacy"))])
|
|
2804
|
+
]) : h("div", { className: "dnm-row" }, [
|
|
2805
|
+
h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-hint" }, t("externalEmpty"))])
|
|
2806
|
+
])],
|
|
2807
|
+
[h("div", { className: "dnm-row" }, [
|
|
2808
|
+
h("div", { className: "dnm-btnRow" }, [
|
|
2809
|
+
h("button", {
|
|
2810
|
+
type: "button",
|
|
2811
|
+
className: "dnm-btn",
|
|
2812
|
+
"aria-label": t("externalAdd"),
|
|
2813
|
+
onClick: addTarget
|
|
2814
|
+
}, t("externalAdd"))
|
|
2815
|
+
])
|
|
2816
|
+
])],
|
|
2817
|
+
[h("div", { className: "dnm-row" }, [
|
|
2818
|
+
h("div", { className: "dnm-rowText" }, [
|
|
2819
|
+
h("div", { className: "dnm-rowLabel" }, t("externalTestLabel")),
|
|
2820
|
+
h("div", { className: "dnm-rowHint" }, t("externalTestHint"))
|
|
2821
|
+
]),
|
|
2822
|
+
h("div", { className: "dnm-btnRow" }, [
|
|
2823
|
+
h("button", {
|
|
2824
|
+
type: "button",
|
|
2825
|
+
className: "dnm-btn",
|
|
2826
|
+
"aria-label": t("externalTestLabel"),
|
|
2827
|
+
disabled: !cfg.enabled,
|
|
2828
|
+
onClick: runExternalTest
|
|
2829
|
+
}, t("externalTestBtn"))
|
|
2830
|
+
])
|
|
2831
|
+
])]
|
|
2832
|
+
)),
|
|
2833
|
+
secTitle(t("secDiag")),
|
|
2834
|
+
card([
|
|
2835
|
+
h("div", { className: "dnm-row" }, [
|
|
2836
|
+
h("div", { className: "dnm-rowText" }, [
|
|
2837
|
+
h("div", { className: "dnm-rowLabel" }, t("hostEnv")),
|
|
2838
|
+
h("div", { className: "dnm-rowHint" }, envHint)
|
|
2839
|
+
]),
|
|
2840
|
+
h("select", {
|
|
2841
|
+
className: "dnm-select",
|
|
2842
|
+
value: cfg.hostEnv,
|
|
2843
|
+
"aria-label": t("hostEnv"),
|
|
2844
|
+
onChange: function (e) { patch({ hostEnv: e.currentTarget.value }); }
|
|
2845
|
+
}, [
|
|
2846
|
+
h("option", { value: "auto" }, t("hostEnvAuto")),
|
|
2847
|
+
h("option", { value: "desktop" }, t("hostEnvDesktop")),
|
|
2848
|
+
h("option", { value: "web" }, t("hostEnvWeb"))
|
|
2849
|
+
])
|
|
2850
|
+
]),
|
|
2851
|
+
h("div", { className: "dnm-row" }, [
|
|
2852
|
+
h("div", { className: "dnm-rowText" }, [
|
|
2853
|
+
h("div", { className: "dnm-rowLabel" }, t("language")),
|
|
2854
|
+
h("div", { className: "dnm-rowHint" }, resolvedLangName + (cfg.language === "auto" ? t("langAutoNote") : ""))
|
|
2855
|
+
]),
|
|
2856
|
+
h("select", {
|
|
2857
|
+
className: "dnm-select",
|
|
2858
|
+
value: cfg.language,
|
|
2859
|
+
"aria-label": t("language"),
|
|
2860
|
+
onChange: function (e) { patch({ language: e.currentTarget.value }); }
|
|
2861
|
+
}, [
|
|
2862
|
+
h("option", { value: "auto" }, t("langAuto")),
|
|
2863
|
+
h("option", { value: "zh" }, t("langZh")),
|
|
2864
|
+
h("option", { value: "en" }, t("langEn"))
|
|
2865
|
+
])
|
|
2866
|
+
]),
|
|
2867
|
+
h("div", { className: "dnm-row" }, [
|
|
2868
|
+
h("div", { className: "dnm-rowText" }, [
|
|
2869
|
+
h("div", { className: "dnm-rowLabel" }, t("reset")),
|
|
2870
|
+
h("div", { className: "dnm-rowHint" }, t("resetHint"))
|
|
2871
|
+
]),
|
|
2872
|
+
h("button", { type: "button", className: "dnm-btn", onClick: function () {
|
|
2873
|
+
// api.resetConfig() also drops the picked cue files, which are
|
|
2874
|
+
// stored outside the config.
|
|
2875
|
+
setCfg(api.resetConfig());
|
|
2876
|
+
applyTitle();
|
|
2877
|
+
setSoundBump(soundBump + 1);
|
|
2878
|
+
setFeedback(t("resetDone"));
|
|
2879
|
+
} }, t("reset"))
|
|
2880
|
+
])
|
|
2881
|
+
])
|
|
2882
|
+
] : null,
|
|
2538
2883
|
// Always-visible version line at the page foot: bug reports need it,
|
|
2539
2884
|
// and "which build am I on" should not require the console.
|
|
2540
2885
|
h("p", { className: "dnm-hint" }, t("versionLine") + api.version)
|
|
@@ -2633,6 +2978,7 @@
|
|
|
2633
2978
|
for (var i = 0; i < unsubs.length; i++) { try { unsubs[i](); } catch (e) {} }
|
|
2634
2979
|
detachFace();
|
|
2635
2980
|
detachUiSession();
|
|
2981
|
+
clearOverlays();
|
|
2636
2982
|
};
|
|
2637
2983
|
}, "dsh-notify-me: watchers");
|
|
2638
2984
|
}
|