@iasiv5/dsh-quota-watch 0.1.13 → 0.1.15
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/README.md +5 -5
- package/lib/client.js +57 -100
- package/package.json +1 -1
- package/src/client/drag.mjs +0 -1
- package/src/client.mjs +83 -104
package/README.md
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
## 功能
|
|
8
8
|
|
|
9
|
-
- **悬浮胶囊**(0.1.1 起为唯一浮动形态,圆球已退役):常驻屏幕(默认右下角安全区),可自由拖拽到屏幕任意位置停放,位置跨重启记忆,越界自动钳回视口(距边 8px 安全边距)。拖拽跟手(保持抓取点,transform
|
|
10
|
-
- **面板**:点击胶囊展开浮动详情面板(半透明毛玻璃底,宽 `min(320px, 100vw−24px)`,靠边自动翻侧,滚动 / 缩放跟随;≤480px 窄屏自动改为底部弹层并避开 safe-area),头部带标题与 ✕,Esc 或点击面板外关闭。概览列两个 provider 行,点击进入详情:GLM 为今日 Tokens / 调用次数双大数字 + 五列对齐窗口网格(MCP(月)→ 5 小时 → 每月 → 每周固定排序,周窗口未下发时显示「每周额度 · ♾️(无限)」占位)+ 分模型用量行;Copilot 为可用额度、本期已用(credits 消耗 +
|
|
11
|
-
-
|
|
12
|
-
- **右键菜单**:显示 / 隐藏侧边栏卡片 · 立即刷新。菜单项随上下文增减(无侧栏卡片挂载点时只剩刷新项)。
|
|
9
|
+
- **悬浮胶囊**(0.1.1 起为唯一浮动形态,圆球已退役):常驻屏幕(默认右下角安全区),可自由拖拽到屏幕任意位置停放,位置跨重启记忆,越界自动钳回视口(距边 8px 安全边距)。拖拽跟手(保持抓取点,transform 渲染);触摸设备可直接拖拽(触摸阈值更宽防误拖)、点击热区扩展到 44px,自动避让 iPhone 刘海 / 小黑条(safe-area)。胶囊一行显示「GLM 13% · Copilot 24%」,已用超过 80% 转橙、超过 95% 转红,任一 provider 超 95% 时胶囊轻微脉动(尊重系统「减少动态效果」设置,开启时不脉动);颜色使用 DSH 主题语义变量,自动适配浅色 / 深色主题。悬停提示与 aria-label 实时给出摘要。单击即面板入口——胶囊上没有任何隐藏手势(0.1.14 起右键 / 长按菜单整体退役)。
|
|
10
|
+
- **面板**:点击胶囊展开浮动详情面板(半透明毛玻璃底,宽 `min(320px, 100vw−24px)`,靠边自动翻侧,滚动 / 缩放跟随;≤480px 窄屏自动改为底部弹层并避开 safe-area),头部带标题与 ✕,Esc 或点击面板外关闭。概览列两个 provider 行,点击进入详情:GLM 为今日 Tokens / 调用次数双大数字 + 五列对齐窗口网格(MCP(月)→ 5 小时 → 每月 → 每周固定排序,周窗口未下发时显示「每周额度 · ♾️(无限)」占位)+ 分模型用量行;Copilot 为可用额度、本期已用(credits 消耗 + 弱化百分比)、重置时间三行。底部一行 **等宽双按钮**(grid 1fr 1fr,宽度与文案长度和语言无关):「显示 / 隐藏侧边栏卡片」(文案随状态翻转)与「立即刷新」(触发宿主立即探测);加载 / 出错空态下按钮同样在场。接口未下发有效 `remaining` / `percent_remaining` 时该行不渲染,`credits_used` 绝不用于推算余额。
|
|
11
|
+
- **侧边栏卡片(可选,默认隐藏)**:胶囊是默认界面;需要侧栏卡片时在面板底部按钮开启「显示侧边栏卡片」(Web profile 侧栏底部区块存在时才出现该按钮)。卡片显示时提供摘要行 + 行点击开面板 + 行首 provider 名称点击立即探测。侧栏收起(Web 窄栏)或容器过窄(<160px,跨 profile 通用判据)时卡片自动隐藏,侧栏整体不存在(Desktop 收起态)同样自然缺席、无报错——入口始终由悬浮胶囊承载。
|
|
13
12
|
- 凭据缺失的 provider 不渲染摘要行与胶囊分段(两者皆缺时悬浮胶囊与卡片一并隐藏);探测失败时保留最后一次成功数据并在行上加 ⚠ 过期标记(⚠ 只出现在面板与卡片行内,不在胶囊上显示)。
|
|
13
|
+
- **语言**:界面文案跟随 DSH「设置 → 语言」——宿主 locale 特性把解析结果实时镜像到 `<html lang>`,插件监听该属性,切换即热重渲染(无需刷新页面);宿主未同步时回退浏览器语言。面板 footer 按钮文字与行内弱化数值(如「3.8亿」)同字号(10px/14px)、同 72% 静息透明度,hover 恢复全强度。
|
|
14
14
|
- 宿主默认每 60 秒探测(窗口 + 用量 + Copilot);profile patch 可将 `pollIntervalSec` 设置在 30–3600 秒。浏览器页签可见时每 30 秒刷新视图,隐藏时暂停。
|
|
15
15
|
|
|
16
16
|
## 兼容与配置
|
|
@@ -47,4 +47,4 @@ dsh plugin --profile web add @iasiv5/dsh-quota-watch
|
|
|
47
47
|
|
|
48
48
|
## English summary
|
|
49
49
|
|
|
50
|
-
DSH Quota Watch is a personal DSH quota monitor built around a persistent floating capsule: a one-line `GLM x% · Copilot y%` summary that turns amber past 80% and red past 95%. Dragging it is grab-accurate (transform-based, rAF-batched) and free to place anywhere on the screen, with position remembered across restarts and viewport boundaries automatically clamped. Touch devices drag directly (wider slop against accidental drags), a 500ms long-press opens the context menu (iOS has no right-click), the hit area is padded to 44px and placement respects `env(safe-area-inset-*)`. Clicking the capsule opens a floating glass panel whose overview reuses the classic 0.0.18 summary rows (label · bar · percent · extra · chevron) and whose detail pages replicate the 0.0.18 big numbers, window grid and per-model list; the panel closes via ✕, Esc or an outside click, and viewports ≤480px dock it to the bottom as a sheet. A sidebar card remains an opportunistic extra entry on web profiles,
|
|
50
|
+
DSH Quota Watch is a personal DSH quota monitor built around a persistent floating capsule: a one-line `GLM x% · Copilot y%` summary that turns amber past 80% and red past 95%. Dragging it is grab-accurate (transform-based, rAF-batched) and free to place anywhere on the screen, with position remembered across restarts and viewport boundaries automatically clamped. Touch devices drag directly (wider slop against accidental drags), a 500ms long-press opens the context menu (iOS has no right-click), the hit area is padded to 44px and placement respects `env(safe-area-inset-*)`. Clicking the capsule opens a floating glass panel whose overview reuses the classic 0.0.18 summary rows (label · bar · percent · extra · chevron) and whose detail pages replicate the 0.0.18 big numbers, window grid and per-model list; the panel closes via ✕, Esc or an outside click, and viewports ≤480px dock it to the bottom as a sheet. The panel footer holds two equal-width buttons — sidebar-card visibility and an immediate provider re-probe — replacing the retired right-click/long-press menu, so the capsule itself carries no hidden gestures. A sidebar card remains an opportunistic extra entry on web profiles, opted in from that footer button, and silently disappears whenever the sidebar does — collapsed web sidebars and the desktop profile are covered by the floating capsule alone. Credentials are resolved on every host-side probe, only normalized values reach the browser, no usage history is kept, and a Copilot balance is never inferred from credits used. UI copy follows the DSH language preference (mirrored live onto `<html lang>`, re-rendering on switch without a reload) and falls back to the browser language; footer buttons rest at the same size and opacity as the muted row extras. Colors ride DSH semantic theme tokens with hardcoded fallbacks; all UI lives inside a shadow root (ADR 0001).
|
package/lib/client.js
CHANGED
|
@@ -152,7 +152,6 @@ function saveSurfaceFlags(storage, flags) {
|
|
|
152
152
|
// src/client/drag.mjs
|
|
153
153
|
var DRAG_SLOP_MOUSE = 6;
|
|
154
154
|
var DRAG_SLOP_TOUCH = 10;
|
|
155
|
-
var LONG_PRESS_MS = 500;
|
|
156
155
|
function dragSlop(pointerType) {
|
|
157
156
|
return pointerType === "touch" || pointerType === "pen" ? DRAG_SLOP_TOUCH : DRAG_SLOP_MOUSE;
|
|
158
157
|
}
|
|
@@ -184,7 +183,7 @@ var COPY = {
|
|
|
184
183
|
title: "Token\u989D\u5EA6",
|
|
185
184
|
openDetails: "\u67E5\u770B\u8BE6\u60C5",
|
|
186
185
|
close: "\u5173\u95ED",
|
|
187
|
-
|
|
186
|
+
actions: {
|
|
188
187
|
showCard: "\u663E\u793A\u4FA7\u8FB9\u680F\u5361\u7247",
|
|
189
188
|
hideCard: "\u9690\u85CF\u4FA7\u8FB9\u680F\u5361\u7247",
|
|
190
189
|
refreshNow: "\u7ACB\u5373\u5237\u65B0"
|
|
@@ -217,7 +216,7 @@ var COPY = {
|
|
|
217
216
|
title: "Quota Watch",
|
|
218
217
|
openDetails: "View details",
|
|
219
218
|
close: "Close",
|
|
220
|
-
|
|
219
|
+
actions: {
|
|
221
220
|
showCard: "Show sidebar card",
|
|
222
221
|
hideCard: "Hide sidebar card",
|
|
223
222
|
refreshNow: "Refresh now"
|
|
@@ -291,10 +290,9 @@ var FLOAT_STYLE_TEXT = `
|
|
|
291
290
|
}
|
|
292
291
|
@media print { :host { display: none !important; } }
|
|
293
292
|
@keyframes dqw-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }
|
|
294
|
-
.dqw-
|
|
295
|
-
.dqw-
|
|
296
|
-
.dqw-
|
|
297
|
-
.dqw-menu-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
|
|
293
|
+
.dqw-panel-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
294
|
+
.dqw-panel-action { min-width: 0; margin: 0; padding: 6px 8px; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 8px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); font: inherit; font-size: 10px; line-height: 14px; opacity: .72; text-align: center; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
295
|
+
.dqw-panel-action:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); opacity: 1; }
|
|
298
296
|
.dqw-panel { position: fixed; z-index: 2147483000; min-width: 166px; max-width: min(320px, calc(100vw - 24px)); max-height: calc(100vh - 24px); overflow-y: auto; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 10px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); background: color-mix(in srgb, var(--dsw-alias-bg-base, #1f1f1f) 86%, transparent); -webkit-backdrop-filter: blur(14px) saturate(1.3); backdrop-filter: blur(14px) saturate(1.3); box-shadow: 0 8px 24px rgba(0,0,0,.25); color: var(--dsw-alias-label-primary, inherit); display: flex; flex-direction: column; gap: 8px; font: inherit; }
|
|
299
297
|
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { .dqw-panel { background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); } }
|
|
300
298
|
.dqw-panel--sheet { left:8px; right:8px; top:auto; bottom:calc(8px + env(safe-area-inset-bottom, 0px)); width:auto; max-width:none; max-height:calc(100dvh - 24px); }
|
|
@@ -347,6 +345,8 @@ var FLOAT_STYLE_TEXT = `
|
|
|
347
345
|
.dqw-error { margin: 0; font-size: 10px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); }
|
|
348
346
|
`;
|
|
349
347
|
function localeFor(win) {
|
|
348
|
+
const host = String(win?.document?.documentElement?.lang ?? "").trim();
|
|
349
|
+
if (host !== "") return host.toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
350
350
|
return String(win?.navigator?.language ?? "zh").toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
351
351
|
}
|
|
352
352
|
function formatNumber(value, locale) {
|
|
@@ -387,15 +387,6 @@ function text(doc, tag, className, value) {
|
|
|
387
387
|
node.textContent = String(value ?? "");
|
|
388
388
|
return node;
|
|
389
389
|
}
|
|
390
|
-
function menuItem(doc, action, label) {
|
|
391
|
-
const item = doc.createElement("button");
|
|
392
|
-
item.type = "button";
|
|
393
|
-
item.className = "dqw-menu-item";
|
|
394
|
-
item.setAttribute("role", "menuitem");
|
|
395
|
-
item.dataset.menu = action;
|
|
396
|
-
item.append(text(doc, "span", "", label));
|
|
397
|
-
return item;
|
|
398
|
-
}
|
|
399
390
|
function backIcon(doc) {
|
|
400
391
|
const namespace = "http://www.w3.org/2000/svg";
|
|
401
392
|
const svg = doc.createElementNS(namespace, "svg");
|
|
@@ -475,8 +466,8 @@ function mountQuotaCard({
|
|
|
475
466
|
};
|
|
476
467
|
if (doc.querySelector(CARD_SELECTOR) || doc.querySelector(FLOAT_SELECTOR)) return () => {
|
|
477
468
|
};
|
|
478
|
-
|
|
479
|
-
|
|
469
|
+
let locale = localeFor(win);
|
|
470
|
+
let copy = COPY[locale];
|
|
480
471
|
const container = doc.createElement("div");
|
|
481
472
|
container.dataset.dshQuotaWatchCard = "";
|
|
482
473
|
container.dataset.dshPlugin = "quota-watch";
|
|
@@ -563,12 +554,6 @@ function mountQuotaCard({
|
|
|
563
554
|
};
|
|
564
555
|
applyPosition();
|
|
565
556
|
floatRoot.append(panel);
|
|
566
|
-
const menu = doc.createElement("div");
|
|
567
|
-
menu.className = "dqw-menu";
|
|
568
|
-
menu.dataset.dshQuotaWatchMenu = "";
|
|
569
|
-
menu.setAttribute("role", "menu");
|
|
570
|
-
menu.hidden = true;
|
|
571
|
-
floatRoot.append(menu);
|
|
572
557
|
let suppressNextClick = false;
|
|
573
558
|
const scheduleFrame = (fn) => typeof win.requestAnimationFrame === "function" ? win.requestAnimationFrame.call(win, fn) : fn();
|
|
574
559
|
const attachDrag = (surfaceEl) => {
|
|
@@ -582,13 +567,6 @@ function mountQuotaCard({
|
|
|
582
567
|
let restX = 0;
|
|
583
568
|
let restY = 0;
|
|
584
569
|
let lastPoint = null;
|
|
585
|
-
let longPressTimer;
|
|
586
|
-
const clearLongPress = () => {
|
|
587
|
-
if (longPressTimer !== void 0) {
|
|
588
|
-
win.clearTimeout(longPressTimer);
|
|
589
|
-
longPressTimer = void 0;
|
|
590
|
-
}
|
|
591
|
-
};
|
|
592
570
|
const bindGesture = () => {
|
|
593
571
|
doc.addEventListener("pointermove", onPointerMove);
|
|
594
572
|
doc.addEventListener("pointerup", onPointerUp);
|
|
@@ -606,19 +584,6 @@ function mountQuotaCard({
|
|
|
606
584
|
originX = event.clientX;
|
|
607
585
|
originY = event.clientY;
|
|
608
586
|
slop = dragSlop(event.pointerType);
|
|
609
|
-
clearLongPress();
|
|
610
|
-
if (slop === DRAG_SLOP_TOUCH) {
|
|
611
|
-
longPressTimer = win.setTimeout(() => {
|
|
612
|
-
longPressTimer = void 0;
|
|
613
|
-
if (!dragging || moved) return;
|
|
614
|
-
try {
|
|
615
|
-
win.navigator.vibrate?.(10);
|
|
616
|
-
} catch {
|
|
617
|
-
}
|
|
618
|
-
openMenu();
|
|
619
|
-
suppressNextClick = true;
|
|
620
|
-
}, LONG_PRESS_MS);
|
|
621
|
-
}
|
|
622
587
|
const rect = surfaceEl.getBoundingClientRect();
|
|
623
588
|
grab = grabOffset({ x: event.clientX, y: event.clientY }, rect);
|
|
624
589
|
dragSize = rect.width > 0 ? { width: rect.width, height: rect.height } : FALLBACK_SIZE;
|
|
@@ -632,7 +597,6 @@ function mountQuotaCard({
|
|
|
632
597
|
if (!moved) {
|
|
633
598
|
if (Math.hypot(event.clientX - originX, event.clientY - originY) < slop) return;
|
|
634
599
|
moved = true;
|
|
635
|
-
clearLongPress();
|
|
636
600
|
doc.body.style.userSelect = "none";
|
|
637
601
|
surfaceEl.classList.add("dqw-capsule--dragging");
|
|
638
602
|
}
|
|
@@ -660,7 +624,6 @@ function mountQuotaCard({
|
|
|
660
624
|
const onPointerUp = (event) => {
|
|
661
625
|
if (!dragging) return;
|
|
662
626
|
dragging = false;
|
|
663
|
-
clearLongPress();
|
|
664
627
|
unbindGesture();
|
|
665
628
|
doc.body.style.userSelect = "";
|
|
666
629
|
if (!moved) return;
|
|
@@ -681,7 +644,6 @@ function mountQuotaCard({
|
|
|
681
644
|
const onPointerCancel = () => {
|
|
682
645
|
if (!dragging) return;
|
|
683
646
|
dragging = false;
|
|
684
|
-
clearLongPress();
|
|
685
647
|
unbindGesture();
|
|
686
648
|
doc.body.style.userSelect = "";
|
|
687
649
|
surfaceEl.classList.remove("dqw-capsule--dragging");
|
|
@@ -779,6 +741,28 @@ function mountQuotaCard({
|
|
|
779
741
|
header.append(close);
|
|
780
742
|
return header;
|
|
781
743
|
};
|
|
744
|
+
const overviewActions = () => {
|
|
745
|
+
const actions = doc.createElement("div");
|
|
746
|
+
actions.className = "dqw-panel-actions";
|
|
747
|
+
if (hasCardMount()) {
|
|
748
|
+
const toggle = doc.createElement("button");
|
|
749
|
+
toggle.type = "button";
|
|
750
|
+
toggle.className = "dqw-panel-action";
|
|
751
|
+
toggle.dataset.action = "toggle-card";
|
|
752
|
+
const toggleLabel = surfaceFlags.cardHidden ? copy.actions.showCard : copy.actions.hideCard;
|
|
753
|
+
toggle.textContent = toggleLabel;
|
|
754
|
+
toggle.setAttribute("aria-label", toggleLabel);
|
|
755
|
+
actions.append(toggle);
|
|
756
|
+
}
|
|
757
|
+
const refresh = doc.createElement("button");
|
|
758
|
+
refresh.type = "button";
|
|
759
|
+
refresh.className = "dqw-panel-action";
|
|
760
|
+
refresh.dataset.action = "panel-refresh";
|
|
761
|
+
refresh.textContent = copy.actions.refreshNow;
|
|
762
|
+
refresh.setAttribute("aria-label", copy.actions.refreshNow);
|
|
763
|
+
actions.append(refresh);
|
|
764
|
+
return actions;
|
|
765
|
+
};
|
|
782
766
|
const renderOverview = () => {
|
|
783
767
|
panel.replaceChildren(panelHeader({ title: copy.title }));
|
|
784
768
|
const providers = snapshot?.providers ?? [];
|
|
@@ -790,15 +774,16 @@ function mountQuotaCard({
|
|
|
790
774
|
].filter(Boolean).flat();
|
|
791
775
|
if (rows.length === 0) {
|
|
792
776
|
panel.append(text(doc, "p", "dqw-muted", lastUpdated.textContent || copy.loading));
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
777
|
+
} else {
|
|
778
|
+
const list = doc.createElement("div");
|
|
779
|
+
list.className = "dqw-overview-list";
|
|
780
|
+
list.append(...rows);
|
|
781
|
+
for (const row of list.querySelectorAll("[data-dsh-quota-watch-row]")) {
|
|
782
|
+
row.dataset.dshQuotaWatchPanelProvider = row.dataset.dshQuotaWatchRow;
|
|
783
|
+
}
|
|
784
|
+
panel.append(list);
|
|
800
785
|
}
|
|
801
|
-
panel.append(
|
|
786
|
+
panel.append(overviewActions());
|
|
802
787
|
};
|
|
803
788
|
function renderDetail(detail, key, provider, fromPanelNav = false) {
|
|
804
789
|
detail.replaceChildren();
|
|
@@ -1008,42 +993,6 @@ function mountQuotaCard({
|
|
|
1008
993
|
onSurfaceClick();
|
|
1009
994
|
};
|
|
1010
995
|
const hasCardMount = () => Boolean(footArea(doc));
|
|
1011
|
-
const openMenu = () => {
|
|
1012
|
-
const items = [];
|
|
1013
|
-
if (hasCardMount()) {
|
|
1014
|
-
items.push(menuItem(doc, "toggle-card", surfaceFlags.cardHidden ? copy.menu.showCard : copy.menu.hideCard));
|
|
1015
|
-
}
|
|
1016
|
-
items.push(menuItem(doc, "refresh", copy.menu.refreshNow));
|
|
1017
|
-
menu.replaceChildren(...items);
|
|
1018
|
-
menu.hidden = false;
|
|
1019
|
-
const rect = surface.getBoundingClientRect();
|
|
1020
|
-
const viewWidth = win.innerWidth ?? 1024;
|
|
1021
|
-
const viewHeight = win.innerHeight ?? 768;
|
|
1022
|
-
const width = menu.offsetWidth || 150;
|
|
1023
|
-
const height = menu.offsetHeight || 120;
|
|
1024
|
-
let left = rect.right + 8;
|
|
1025
|
-
if (left + width > viewWidth - 8) left = Math.max(8, rect.left - width - 8);
|
|
1026
|
-
const top = Math.min(Math.max(rect.top, 8), Math.max(8, viewHeight - height - 8));
|
|
1027
|
-
menu.style.left = `${Math.round(left)}px`;
|
|
1028
|
-
menu.style.top = `${Math.round(top)}px`;
|
|
1029
|
-
};
|
|
1030
|
-
const onSurfaceContext = (event) => {
|
|
1031
|
-
event.preventDefault();
|
|
1032
|
-
if (!menu.hidden) return;
|
|
1033
|
-
openMenu();
|
|
1034
|
-
};
|
|
1035
|
-
const onMenuClick = (event) => {
|
|
1036
|
-
const item = event.target?.closest?.("[data-menu]");
|
|
1037
|
-
if (!item) return;
|
|
1038
|
-
const action = item.dataset.menu;
|
|
1039
|
-
if (action === "toggle-card") {
|
|
1040
|
-
setCardHidden(!surfaceFlags.cardHidden);
|
|
1041
|
-
} else if (action === "refresh") {
|
|
1042
|
-
void poll(true);
|
|
1043
|
-
}
|
|
1044
|
-
menu.hidden = true;
|
|
1045
|
-
};
|
|
1046
|
-
menu.addEventListener("click", onMenuClick);
|
|
1047
996
|
let currentFoot = null;
|
|
1048
997
|
let footTooNarrow = false;
|
|
1049
998
|
let noDataHidden = false;
|
|
@@ -1060,6 +1009,8 @@ function mountQuotaCard({
|
|
|
1060
1009
|
syncCardVisibility();
|
|
1061
1010
|
}) : null;
|
|
1062
1011
|
const renderAll = () => {
|
|
1012
|
+
locale = localeFor(win);
|
|
1013
|
+
copy = COPY[locale];
|
|
1063
1014
|
const providers = snapshot?.providers ?? [];
|
|
1064
1015
|
const glm = providers.find((provider) => provider?.key === "glm");
|
|
1065
1016
|
const copilot = providers.find((provider) => provider?.key === "copilot");
|
|
@@ -1124,6 +1075,10 @@ function mountQuotaCard({
|
|
|
1124
1075
|
schedulePlace();
|
|
1125
1076
|
});
|
|
1126
1077
|
observer.observe(doc.body, { childList: true, subtree: true });
|
|
1078
|
+
const langObserver = typeof win.MutationObserver === "function" ? new win.MutationObserver(() => {
|
|
1079
|
+
if (!disposed) renderAll();
|
|
1080
|
+
}) : null;
|
|
1081
|
+
langObserver?.observe(doc.documentElement, { attributeFilter: ["lang"] });
|
|
1127
1082
|
place();
|
|
1128
1083
|
const startPolling = () => {
|
|
1129
1084
|
if (timer !== void 0 || doc.visibilityState === "hidden" || disposed) return;
|
|
@@ -1153,6 +1108,14 @@ function mountQuotaCard({
|
|
|
1153
1108
|
syncPop();
|
|
1154
1109
|
};
|
|
1155
1110
|
const onPanelClick = (event) => {
|
|
1111
|
+
if (event.target?.closest?.('[data-action="panel-refresh"]')) {
|
|
1112
|
+
void poll(true);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
if (event.target?.closest?.('[data-action="toggle-card"]')) {
|
|
1116
|
+
setCardHidden(!surfaceFlags.cardHidden);
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1156
1119
|
if (event.target?.closest?.('[data-action="panel-close"]')) {
|
|
1157
1120
|
openKey = void 0;
|
|
1158
1121
|
syncPop();
|
|
@@ -1189,9 +1152,9 @@ function mountQuotaCard({
|
|
|
1189
1152
|
const onDocPointerDown = (event) => {
|
|
1190
1153
|
if (openKey === void 0) return;
|
|
1191
1154
|
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
|
|
1192
|
-
if (path.includes(panel) || path.includes(
|
|
1155
|
+
if (path.includes(panel) || path.includes(container)) return;
|
|
1193
1156
|
const target = event.target;
|
|
1194
|
-
if (target === panel || target ===
|
|
1157
|
+
if (target === panel || target === container) return;
|
|
1195
1158
|
if (target?.getRootNode?.() === cardRoot || target?.getRootNode?.() === floatRoot) return;
|
|
1196
1159
|
openKey = void 0;
|
|
1197
1160
|
renderAll();
|
|
@@ -1212,10 +1175,6 @@ function mountQuotaCard({
|
|
|
1212
1175
|
};
|
|
1213
1176
|
const onDocKeydown = (event) => {
|
|
1214
1177
|
if (event.key !== "Escape") return;
|
|
1215
|
-
if (!menu.hidden) {
|
|
1216
|
-
menu.hidden = true;
|
|
1217
|
-
return;
|
|
1218
|
-
}
|
|
1219
1178
|
if (openKey !== void 0) {
|
|
1220
1179
|
openKey = void 0;
|
|
1221
1180
|
renderAll();
|
|
@@ -1235,7 +1194,6 @@ function mountQuotaCard({
|
|
|
1235
1194
|
});
|
|
1236
1195
|
surface.addEventListener("click", onSurfaceClick);
|
|
1237
1196
|
surface.addEventListener("keydown", onSurfaceKeydown);
|
|
1238
|
-
surface.addEventListener("contextmenu", onSurfaceContext);
|
|
1239
1197
|
let disposeDrag = attachDrag(surface);
|
|
1240
1198
|
doc.addEventListener("visibilitychange", onVisibilityChange);
|
|
1241
1199
|
doc.addEventListener("pointerdown", onDocPointerDown, true);
|
|
@@ -1247,6 +1205,7 @@ function mountQuotaCard({
|
|
|
1247
1205
|
disposed = true;
|
|
1248
1206
|
stopPolling();
|
|
1249
1207
|
observer.disconnect();
|
|
1208
|
+
langObserver?.disconnect();
|
|
1250
1209
|
doc.removeEventListener("visibilitychange", onVisibilityChange);
|
|
1251
1210
|
doc.removeEventListener("pointerdown", onDocPointerDown, true);
|
|
1252
1211
|
doc.removeEventListener("scroll", onDocScroll, true);
|
|
@@ -1257,8 +1216,6 @@ function mountQuotaCard({
|
|
|
1257
1216
|
panel.removeEventListener("click", onPanelClick);
|
|
1258
1217
|
surface.removeEventListener("click", onSurfaceClick);
|
|
1259
1218
|
surface.removeEventListener("keydown", onSurfaceKeydown);
|
|
1260
|
-
surface.removeEventListener("contextmenu", onSurfaceContext);
|
|
1261
|
-
menu.removeEventListener("click", onMenuClick);
|
|
1262
1219
|
disposeDrag();
|
|
1263
1220
|
doc.body.style.userSelect = "";
|
|
1264
1221
|
container.remove();
|
package/package.json
CHANGED
package/src/client/drag.mjs
CHANGED
package/src/client.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CLIENT_POLL_INTERVAL_MS, CLIENT_ROUTES } from './shared.mjs'
|
|
2
2
|
import { remainingToUsed } from './core/adapters.mjs'
|
|
3
3
|
import { MARGIN, clampPoint, loadFloatPosition, loadSurfaceFlags, saveFloatPosition, saveSurfaceFlags } from './client/prefs.mjs'
|
|
4
|
-
import { DRAG_SLOP_MOUSE, DRAG_SLOP_TOUCH,
|
|
4
|
+
import { DRAG_SLOP_MOUSE, DRAG_SLOP_TOUCH, parseInset, clampFrame, dragSlop, grabOffset } from './client/drag.mjs'
|
|
5
5
|
|
|
6
6
|
export const name = 'quota-watch-client'
|
|
7
7
|
export const inject = []
|
|
@@ -23,7 +23,7 @@ const COPY = {
|
|
|
23
23
|
title: 'Token额度',
|
|
24
24
|
openDetails: '查看详情',
|
|
25
25
|
close: '关闭',
|
|
26
|
-
|
|
26
|
+
actions: {
|
|
27
27
|
showCard: '显示侧边栏卡片',
|
|
28
28
|
hideCard: '隐藏侧边栏卡片',
|
|
29
29
|
refreshNow: '立即刷新',
|
|
@@ -56,7 +56,7 @@ const COPY = {
|
|
|
56
56
|
title: 'Quota Watch',
|
|
57
57
|
openDetails: 'View details',
|
|
58
58
|
close: 'Close',
|
|
59
|
-
|
|
59
|
+
actions: {
|
|
60
60
|
showCard: 'Show sidebar card',
|
|
61
61
|
hideCard: 'Hide sidebar card',
|
|
62
62
|
refreshNow: 'Refresh now',
|
|
@@ -137,10 +137,9 @@ const FLOAT_STYLE_TEXT = `
|
|
|
137
137
|
}
|
|
138
138
|
@media print { :host { display: none !important; } }
|
|
139
139
|
@keyframes dqw-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }
|
|
140
|
-
.dqw-
|
|
141
|
-
.dqw-
|
|
142
|
-
.dqw-
|
|
143
|
-
.dqw-menu-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
|
|
140
|
+
.dqw-panel-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
141
|
+
.dqw-panel-action { min-width: 0; margin: 0; padding: 6px 8px; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 8px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); font: inherit; font-size: 10px; line-height: 14px; opacity: .72; text-align: center; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
142
|
+
.dqw-panel-action:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); opacity: 1; }
|
|
144
143
|
.dqw-panel { position: fixed; z-index: 2147483000; min-width: 166px; max-width: min(320px, calc(100vw - 24px)); max-height: calc(100vh - 24px); overflow-y: auto; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 10px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); background: color-mix(in srgb, var(--dsw-alias-bg-base, #1f1f1f) 86%, transparent); -webkit-backdrop-filter: blur(14px) saturate(1.3); backdrop-filter: blur(14px) saturate(1.3); box-shadow: 0 8px 24px rgba(0,0,0,.25); color: var(--dsw-alias-label-primary, inherit); display: flex; flex-direction: column; gap: 8px; font: inherit; }
|
|
145
144
|
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { .dqw-panel { background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); } }
|
|
146
145
|
.dqw-panel--sheet { left:8px; right:8px; top:auto; bottom:calc(8px + env(safe-area-inset-bottom, 0px)); width:auto; max-width:none; max-height:calc(100dvh - 24px); }
|
|
@@ -193,7 +192,13 @@ const FLOAT_STYLE_TEXT = `
|
|
|
193
192
|
.dqw-error { margin: 0; font-size: 10px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); }
|
|
194
193
|
`
|
|
195
194
|
|
|
195
|
+
/** Locale resolution follows the host, not just the browser: the DSH locale
|
|
196
|
+
* feature mirrors its resolved preference (设置 → 语言) onto
|
|
197
|
+
* `<html lang>` and keeps it updated on every switch, so prefer that signal
|
|
198
|
+
* and fall back to navigator.language when the host has not synced yet. */
|
|
196
199
|
function localeFor(win) {
|
|
200
|
+
const host = String(win?.document?.documentElement?.lang ?? '').trim()
|
|
201
|
+
if (host !== '') return host.toLowerCase().startsWith('zh') ? 'zh' : 'en'
|
|
197
202
|
return String(win?.navigator?.language ?? 'zh').toLowerCase().startsWith('zh') ? 'zh' : 'en'
|
|
198
203
|
}
|
|
199
204
|
|
|
@@ -271,17 +276,6 @@ function quotaIcon(doc) {
|
|
|
271
276
|
return svg
|
|
272
277
|
}
|
|
273
278
|
|
|
274
|
-
/** One right-click menu row. */
|
|
275
|
-
function menuItem(doc, action, label) {
|
|
276
|
-
const item = doc.createElement('button')
|
|
277
|
-
item.type = 'button'
|
|
278
|
-
item.className = 'dqw-menu-item'
|
|
279
|
-
item.setAttribute('role', 'menuitem')
|
|
280
|
-
item.dataset.menu = action
|
|
281
|
-
item.append(text(doc, 'span', '', label))
|
|
282
|
-
return item
|
|
283
|
-
}
|
|
284
|
-
|
|
285
279
|
/** Double-chevron icon for the panel's back affordance. */
|
|
286
280
|
function backIcon(doc) {
|
|
287
281
|
const namespace = 'http://www.w3.org/2000/svg'
|
|
@@ -373,8 +367,11 @@ export function mountQuotaCard({
|
|
|
373
367
|
if (!doc?.body || typeof fetchImpl !== 'function') return () => {}
|
|
374
368
|
if (doc.querySelector(CARD_SELECTOR) || doc.querySelector(FLOAT_SELECTOR)) return () => {}
|
|
375
369
|
|
|
376
|
-
|
|
377
|
-
|
|
370
|
+
// Mutable so a host language switch (设置 → 语言 → <html lang>) re-renders
|
|
371
|
+
// every surface with the new dictionary on the next pass — renderAll is the
|
|
372
|
+
// one funnel all render paths share.
|
|
373
|
+
let locale = localeFor(win)
|
|
374
|
+
let copy = COPY[locale]
|
|
378
375
|
const container = doc.createElement('div')
|
|
379
376
|
container.dataset.dshQuotaWatchCard = ''
|
|
380
377
|
container.dataset.dshPlugin = 'quota-watch'
|
|
@@ -480,12 +477,6 @@ export function mountQuotaCard({
|
|
|
480
477
|
applyPosition()
|
|
481
478
|
// Panel lives inside the float shadow root from now on (契约总表「归属时序」).
|
|
482
479
|
floatRoot.append(panel)
|
|
483
|
-
const menu = doc.createElement('div')
|
|
484
|
-
menu.className = 'dqw-menu'
|
|
485
|
-
menu.dataset.dshQuotaWatchMenu = ''
|
|
486
|
-
menu.setAttribute('role', 'menu')
|
|
487
|
-
menu.hidden = true
|
|
488
|
-
floatRoot.append(menu)
|
|
489
480
|
|
|
490
481
|
let suppressNextClick = false
|
|
491
482
|
/** One rAF (or a synchronous fallback when the host has none) — drag frames
|
|
@@ -511,13 +502,6 @@ export function mountQuotaCard({
|
|
|
511
502
|
let restX = 0
|
|
512
503
|
let restY = 0
|
|
513
504
|
let lastPoint = null
|
|
514
|
-
let longPressTimer
|
|
515
|
-
const clearLongPress = () => {
|
|
516
|
-
if (longPressTimer !== undefined) {
|
|
517
|
-
win.clearTimeout(longPressTimer)
|
|
518
|
-
longPressTimer = undefined
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
505
|
/** Doc-level move/up/cancel are bound only while a gesture is active —
|
|
522
506
|
* the pill's resting state costs zero document-level listeners. */
|
|
523
507
|
const bindGesture = () => {
|
|
@@ -538,18 +522,6 @@ export function mountQuotaCard({
|
|
|
538
522
|
originY = event.clientY
|
|
539
523
|
// Graded slop: touch/pen get 10px so taps win over drags (Task 6).
|
|
540
524
|
slop = dragSlop(event.pointerType)
|
|
541
|
-
// Long press opens the context menu on touch (iOS has no contextmenu);
|
|
542
|
-
// movement past the slop cancels it in favor of the drag.
|
|
543
|
-
clearLongPress()
|
|
544
|
-
if (slop === DRAG_SLOP_TOUCH) {
|
|
545
|
-
longPressTimer = win.setTimeout(() => {
|
|
546
|
-
longPressTimer = undefined
|
|
547
|
-
if (!dragging || moved) return
|
|
548
|
-
try { win.navigator.vibrate?.(10) } catch { /* vibration unavailable */ }
|
|
549
|
-
openMenu()
|
|
550
|
-
suppressNextClick = true
|
|
551
|
-
}, LONG_PRESS_MS)
|
|
552
|
-
}
|
|
553
525
|
const rect = surfaceEl.getBoundingClientRect()
|
|
554
526
|
grab = grabOffset({ x: event.clientX, y: event.clientY }, rect)
|
|
555
527
|
dragSize = rect.width > 0 ? { width: rect.width, height: rect.height } : FALLBACK_SIZE
|
|
@@ -563,7 +535,6 @@ export function mountQuotaCard({
|
|
|
563
535
|
if (!moved) {
|
|
564
536
|
if (Math.hypot(event.clientX - originX, event.clientY - originY) < slop) return
|
|
565
537
|
moved = true
|
|
566
|
-
clearLongPress()
|
|
567
538
|
doc.body.style.userSelect = 'none'
|
|
568
539
|
surfaceEl.classList.add('dqw-capsule--dragging')
|
|
569
540
|
}
|
|
@@ -591,7 +562,6 @@ export function mountQuotaCard({
|
|
|
591
562
|
const onPointerUp = (event) => {
|
|
592
563
|
if (!dragging) return
|
|
593
564
|
dragging = false
|
|
594
|
-
clearLongPress()
|
|
595
565
|
unbindGesture()
|
|
596
566
|
doc.body.style.userSelect = ''
|
|
597
567
|
if (!moved) return
|
|
@@ -614,7 +584,6 @@ export function mountQuotaCard({
|
|
|
614
584
|
const onPointerCancel = () => {
|
|
615
585
|
if (!dragging) return
|
|
616
586
|
dragging = false
|
|
617
|
-
clearLongPress()
|
|
618
587
|
unbindGesture()
|
|
619
588
|
doc.body.style.userSelect = ''
|
|
620
589
|
surfaceEl.classList.remove('dqw-capsule--dragging')
|
|
@@ -719,6 +688,34 @@ export function mountQuotaCard({
|
|
|
719
688
|
return header
|
|
720
689
|
}
|
|
721
690
|
|
|
691
|
+
/** Panel footer: the card toggle and a manual refresh sit side by side in an
|
|
692
|
+
* equal-width grid (perceived width is the box, not the glyph count, so the
|
|
693
|
+
* 1fr/1fr columns also survive label flips and other locales). This replaces
|
|
694
|
+
* the retired capsule context menu: one click surface, no hidden gestures,
|
|
695
|
+
* and it renders even in the loading/error empty state. */
|
|
696
|
+
const overviewActions = () => {
|
|
697
|
+
const actions = doc.createElement('div')
|
|
698
|
+
actions.className = 'dqw-panel-actions'
|
|
699
|
+
if (hasCardMount()) {
|
|
700
|
+
const toggle = doc.createElement('button')
|
|
701
|
+
toggle.type = 'button'
|
|
702
|
+
toggle.className = 'dqw-panel-action'
|
|
703
|
+
toggle.dataset.action = 'toggle-card'
|
|
704
|
+
const toggleLabel = surfaceFlags.cardHidden ? copy.actions.showCard : copy.actions.hideCard
|
|
705
|
+
toggle.textContent = toggleLabel
|
|
706
|
+
toggle.setAttribute('aria-label', toggleLabel)
|
|
707
|
+
actions.append(toggle)
|
|
708
|
+
}
|
|
709
|
+
const refresh = doc.createElement('button')
|
|
710
|
+
refresh.type = 'button'
|
|
711
|
+
refresh.className = 'dqw-panel-action'
|
|
712
|
+
refresh.dataset.action = 'panel-refresh'
|
|
713
|
+
refresh.textContent = copy.actions.refreshNow
|
|
714
|
+
refresh.setAttribute('aria-label', copy.actions.refreshNow)
|
|
715
|
+
actions.append(refresh)
|
|
716
|
+
return actions
|
|
717
|
+
}
|
|
718
|
+
|
|
722
719
|
const renderOverview = () => {
|
|
723
720
|
panel.replaceChildren(panelHeader({ title: copy.title }))
|
|
724
721
|
const providers = snapshot?.providers ?? []
|
|
@@ -733,16 +730,17 @@ export function mountQuotaCard({
|
|
|
733
730
|
].filter(Boolean).flat()
|
|
734
731
|
if (rows.length === 0) {
|
|
735
732
|
panel.append(text(doc, 'p', 'dqw-muted', lastUpdated.textContent || copy.loading))
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
733
|
+
} else {
|
|
734
|
+
const list = doc.createElement('div')
|
|
735
|
+
list.className = 'dqw-overview-list'
|
|
736
|
+
list.append(...rows)
|
|
737
|
+
// Tag the reused rows for the panel's own click/keyboard routing.
|
|
738
|
+
for (const row of list.querySelectorAll('[data-dsh-quota-watch-row]')) {
|
|
739
|
+
row.dataset.dshQuotaWatchPanelProvider = row.dataset.dshQuotaWatchRow
|
|
740
|
+
}
|
|
741
|
+
panel.append(list)
|
|
744
742
|
}
|
|
745
|
-
panel.append(
|
|
743
|
+
panel.append(overviewActions())
|
|
746
744
|
}
|
|
747
745
|
|
|
748
746
|
function renderDetail(detail, key, provider, fromPanelNav = false) {
|
|
@@ -973,44 +971,6 @@ export function mountQuotaCard({
|
|
|
973
971
|
onSurfaceClick()
|
|
974
972
|
}
|
|
975
973
|
const hasCardMount = () => Boolean(footArea(doc))
|
|
976
|
-
const openMenu = () => {
|
|
977
|
-
const items = []
|
|
978
|
-
if (hasCardMount()) {
|
|
979
|
-
items.push(menuItem(doc, 'toggle-card', surfaceFlags.cardHidden ? copy.menu.showCard : copy.menu.hideCard))
|
|
980
|
-
}
|
|
981
|
-
items.push(menuItem(doc, 'refresh', copy.menu.refreshNow))
|
|
982
|
-
menu.replaceChildren(...items)
|
|
983
|
-
menu.hidden = false
|
|
984
|
-
const rect = surface.getBoundingClientRect()
|
|
985
|
-
const viewWidth = win.innerWidth ?? 1024
|
|
986
|
-
const viewHeight = win.innerHeight ?? 768
|
|
987
|
-
const width = menu.offsetWidth || 150
|
|
988
|
-
const height = menu.offsetHeight || 120
|
|
989
|
-
let left = rect.right + 8
|
|
990
|
-
if (left + width > viewWidth - 8) left = Math.max(8, rect.left - width - 8)
|
|
991
|
-
const top = Math.min(Math.max(rect.top, 8), Math.max(8, viewHeight - height - 8))
|
|
992
|
-
menu.style.left = `${Math.round(left)}px`
|
|
993
|
-
menu.style.top = `${Math.round(top)}px`
|
|
994
|
-
}
|
|
995
|
-
const onSurfaceContext = (event) => {
|
|
996
|
-
event.preventDefault()
|
|
997
|
-
// Android fires the native contextmenu right after the long-press timer:
|
|
998
|
-
// an already-open menu is kept as-is instead of being rebuilt.
|
|
999
|
-
if (!menu.hidden) return
|
|
1000
|
-
openMenu()
|
|
1001
|
-
}
|
|
1002
|
-
const onMenuClick = (event) => {
|
|
1003
|
-
const item = event.target?.closest?.('[data-menu]')
|
|
1004
|
-
if (!item) return
|
|
1005
|
-
const action = item.dataset.menu
|
|
1006
|
-
if (action === 'toggle-card') {
|
|
1007
|
-
setCardHidden(!surfaceFlags.cardHidden)
|
|
1008
|
-
} else if (action === 'refresh') {
|
|
1009
|
-
void poll(true)
|
|
1010
|
-
}
|
|
1011
|
-
menu.hidden = true
|
|
1012
|
-
}
|
|
1013
|
-
menu.addEventListener('click', onMenuClick)
|
|
1014
974
|
|
|
1015
975
|
// Cross-profile narrow-container detection: a measurably narrow foot would
|
|
1016
976
|
// clip the card into unreadable text regardless of HOW the host signals its
|
|
@@ -1039,6 +999,8 @@ export function mountQuotaCard({
|
|
|
1039
999
|
? new win.ResizeObserver(() => { syncCardVisibility() })
|
|
1040
1000
|
: null
|
|
1041
1001
|
const renderAll = () => {
|
|
1002
|
+
locale = localeFor(win)
|
|
1003
|
+
copy = COPY[locale]
|
|
1042
1004
|
const providers = snapshot?.providers ?? []
|
|
1043
1005
|
const glm = providers.find((provider) => provider?.key === 'glm')
|
|
1044
1006
|
const copilot = providers.find((provider) => provider?.key === 'copilot')
|
|
@@ -1112,6 +1074,13 @@ export function mountQuotaCard({
|
|
|
1112
1074
|
schedulePlace()
|
|
1113
1075
|
})
|
|
1114
1076
|
observer.observe(doc.body, { childList: true, subtree: true })
|
|
1077
|
+
// The DSH locale feature mirrors every language switch onto <html lang>;
|
|
1078
|
+
// re-render on that flip so the UI follows 设置 → 语言 without a reload.
|
|
1079
|
+
// A no-op flip (same value) never fires, so this costs nothing at rest.
|
|
1080
|
+
const langObserver = typeof win.MutationObserver === 'function'
|
|
1081
|
+
? new win.MutationObserver(() => { if (!disposed) renderAll() })
|
|
1082
|
+
: null
|
|
1083
|
+
langObserver?.observe(doc.documentElement, { attributeFilter: ['lang'] })
|
|
1115
1084
|
place()
|
|
1116
1085
|
|
|
1117
1086
|
const startPolling = () => {
|
|
@@ -1140,6 +1109,19 @@ export function mountQuotaCard({
|
|
|
1140
1109
|
syncPop()
|
|
1141
1110
|
}
|
|
1142
1111
|
const onPanelClick = (event) => {
|
|
1112
|
+
// Panel-scoped actions (the retired context menu now lives here): buttons
|
|
1113
|
+
// sit inside this shadow tree, so a panel-level listener sees the real
|
|
1114
|
+
// target with no retargeting ambiguity.
|
|
1115
|
+
if (event.target?.closest?.('[data-action="panel-refresh"]')) {
|
|
1116
|
+
void poll(true)
|
|
1117
|
+
return
|
|
1118
|
+
}
|
|
1119
|
+
if (event.target?.closest?.('[data-action="toggle-card"]')) {
|
|
1120
|
+
// renderAll → syncPop → renderOverview re-renders the footer, so the
|
|
1121
|
+
// label flips in place while the panel stays open.
|
|
1122
|
+
setCardHidden(!surfaceFlags.cardHidden)
|
|
1123
|
+
return
|
|
1124
|
+
}
|
|
1143
1125
|
if (event.target?.closest?.('[data-action="panel-close"]')) {
|
|
1144
1126
|
openKey = undefined
|
|
1145
1127
|
syncPop()
|
|
@@ -1157,6 +1139,9 @@ export function mountQuotaCard({
|
|
|
1157
1139
|
syncPop()
|
|
1158
1140
|
}
|
|
1159
1141
|
}
|
|
1142
|
+
// Both handlers ride the card's inner body element — INSIDE the card shadow
|
|
1143
|
+
// root — so shadow-host retargeting never applies and event.target is the
|
|
1144
|
+
// real row/label in every browser.
|
|
1160
1145
|
const onBodyClick = (event) => {
|
|
1161
1146
|
if (event.target?.closest?.('[data-action="refresh"]')) {
|
|
1162
1147
|
void poll(true)
|
|
@@ -1176,9 +1161,9 @@ export function mountQuotaCard({
|
|
|
1176
1161
|
const onDocPointerDown = (event) => {
|
|
1177
1162
|
if (openKey === undefined) return
|
|
1178
1163
|
const path = typeof event.composedPath === 'function' ? event.composedPath() : []
|
|
1179
|
-
if (path.includes(panel) || path.includes(
|
|
1164
|
+
if (path.includes(panel) || path.includes(container)) return
|
|
1180
1165
|
const target = event.target
|
|
1181
|
-
if (target === panel || target ===
|
|
1166
|
+
if (target === panel || target === container) return
|
|
1182
1167
|
if (target?.getRootNode?.() === cardRoot || target?.getRootNode?.() === floatRoot) return
|
|
1183
1168
|
openKey = undefined
|
|
1184
1169
|
renderAll()
|
|
@@ -1200,10 +1185,6 @@ export function mountQuotaCard({
|
|
|
1200
1185
|
}
|
|
1201
1186
|
const onDocKeydown = (event) => {
|
|
1202
1187
|
if (event.key !== 'Escape') return
|
|
1203
|
-
if (!menu.hidden) {
|
|
1204
|
-
menu.hidden = true
|
|
1205
|
-
return
|
|
1206
|
-
}
|
|
1207
1188
|
if (openKey !== undefined) {
|
|
1208
1189
|
openKey = undefined
|
|
1209
1190
|
renderAll()
|
|
@@ -1223,7 +1204,6 @@ export function mountQuotaCard({
|
|
|
1223
1204
|
})
|
|
1224
1205
|
surface.addEventListener('click', onSurfaceClick)
|
|
1225
1206
|
surface.addEventListener('keydown', onSurfaceKeydown)
|
|
1226
|
-
surface.addEventListener('contextmenu', onSurfaceContext)
|
|
1227
1207
|
let disposeDrag = attachDrag(surface)
|
|
1228
1208
|
doc.addEventListener('visibilitychange', onVisibilityChange)
|
|
1229
1209
|
doc.addEventListener('pointerdown', onDocPointerDown, true)
|
|
@@ -1236,6 +1216,7 @@ export function mountQuotaCard({
|
|
|
1236
1216
|
disposed = true
|
|
1237
1217
|
stopPolling()
|
|
1238
1218
|
observer.disconnect()
|
|
1219
|
+
langObserver?.disconnect()
|
|
1239
1220
|
doc.removeEventListener('visibilitychange', onVisibilityChange)
|
|
1240
1221
|
doc.removeEventListener('pointerdown', onDocPointerDown, true)
|
|
1241
1222
|
doc.removeEventListener('scroll', onDocScroll, true)
|
|
@@ -1246,8 +1227,6 @@ export function mountQuotaCard({
|
|
|
1246
1227
|
panel.removeEventListener('click', onPanelClick)
|
|
1247
1228
|
surface.removeEventListener('click', onSurfaceClick)
|
|
1248
1229
|
surface.removeEventListener('keydown', onSurfaceKeydown)
|
|
1249
|
-
surface.removeEventListener('contextmenu', onSurfaceContext)
|
|
1250
|
-
menu.removeEventListener('click', onMenuClick)
|
|
1251
1230
|
disposeDrag()
|
|
1252
1231
|
// A dispose landing mid-gesture must not leave the body unselectable.
|
|
1253
1232
|
doc.body.style.userSelect = ''
|