@cowwo/dsh-icon-custom 0.15.0 → 0.16.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/README.zh.md +1 -1
- package/client/client.js +76 -21
- package/package.json +1 -1
package/README.zh.md
CHANGED
|
@@ -58,7 +58,7 @@ SVG 会在浏览器里先栅格化成 512×512 PNG 再上传,ICO 则由后端提
|
|
|
58
58
|
规则:
|
|
59
59
|
|
|
60
60
|
- 数字 = **几个会话**在等你(不是几件事);同一个会话只占一格。
|
|
61
|
-
- 只有子代理会话不计。**什么算"你在看"**(0.
|
|
61
|
+
- 只有子代理会话不计。**什么算"你在看"**(0.15.2 起):那个会话必须真的显示在主区域的对话里、浏览器标签页在前台(切到别的标签页或最小化不算)、且屏幕上就是它——残留的"最近打开过"不再算数。**这两种"不算"都只是暂停计时,不会因此清掉红点**;真正算"离开"的只有一件事:**去别的会话**(那时它的快照被丢弃,你下次进来待够 N 秒才算看过)。**你正开着的那个会话也算**;**要清掉它,切走再切回来即可**(你人已经在里面时又结束的那一轮不会自动消,见下一条)。
|
|
62
62
|
- **已读记在宿主**(`$DSH_HOME/custom-favicon/unread-seen.json`):进过 = 处理过,刷新/重启不会"复活",**任何一台设备上看过,所有设备都不再提醒**(开着的页面最多约一分钟收敛,刷新则立刻)。浏览器自己也留一份,但只当缓存兼离线队列——所以清站点数据、换浏览器、无痕窗口都不会把"已读"丢回去。装插件之前的历史结束也不算。
|
|
63
63
|
- **可以要求"待够才算已读"**:设置页填一个秒数(默认 0)。填 0 = 进去就算已读(红点立即消失);填 N = 你要在那个会话里**连续待满 N 秒**它才算看过,中途切走就清零重数,刷新页面也重新数。上限 600 秒。
|
|
64
64
|
**计时只认"你进入会话时已经存在的那次结束"**:你人已经在会话里的时候它又跑完一轮,那一轮**不算数**——红点会一直亮着,等你**切走再进来**才消。这样"我正看着的会话跑完了"这件事不会被几秒钟后自动抹掉(0.11.1 起;此前是"停留满 N 秒就把最新那次结束也标成已读",红点只闪一下)。
|
package/client/client.js
CHANGED
|
@@ -80,7 +80,7 @@ window.__ModuleLoader__.load({
|
|
|
80
80
|
badgeTestHint: "真实未读 = 有会话发生了上面勾选的情况、而且你还没看过它(打开该会话即视为已读)。你正开着的会话也算——窗口可能被最小化、页面可能切到后台,这里无从判断你在不在看,所以不做这个区分。只有子代理不计。手动模式:自己填数字试看效果。刷新页面后回到真实未读。",
|
|
81
81
|
clearDelayLabel: "进入会话后多久算已读:",
|
|
82
82
|
clearDelayUnit: "秒",
|
|
83
|
-
clearDelayHint: "填 0 = 进去就算已读(红点立即消失)。大于 0 时,你要在那个会话里连续待满这么多秒它才算看过:中途切走会清零重数,刷新页面也重新数。**计时只认\"你进入会话时已经存在的结束\"**——你人已经在里面的时候又跑完一轮,那一轮不算数:它会一直亮着,等你切走再进来才会清。上限 600
|
|
83
|
+
clearDelayHint: "填 0 = 进去就算已读(红点立即消失)。大于 0 时,你要在那个会话里连续待满这么多秒它才算看过:中途切走会清零重数,刷新页面也重新数。**计时只认\"你进入会话时已经存在的结束\"**——你人已经在里面的时候又跑完一轮,那一轮不算数:它会一直亮着,等你切走再进来才会清。上限 600 秒。**切到「插件/设置」页、或把标签页切到后台,都只是暂停计时,不会因此清掉红点**(它们不算离开);真正算离开的只有一件事:**去别的会话**。已读记在宿主($DSH_HOME/custom-favicon/unread-seen.json):任何一台设备上看过,所有设备都不再提醒;浏览器那份只是缓存,清站点数据不会把已读丢回去。"
|
|
84
84
|
};
|
|
85
85
|
const en = {
|
|
86
86
|
nav: "Favicon",
|
|
@@ -153,7 +153,7 @@ window.__ModuleLoader__.load({
|
|
|
153
153
|
badgeTestHint: "Real unread = a session ended for one of the checked reasons and you have not looked at it yet (opening a session marks it read). The session you are viewing counts too — the window may be minimised or the page in the background, so this half cannot tell whether you are looking, and does not pretend to. Only sub-agents never count. Manual = type a number to preview. Resets to Real on reload.",
|
|
154
154
|
clearDelayLabel: "Mark read after staying:",
|
|
155
155
|
clearDelayUnit: "seconds",
|
|
156
|
-
clearDelayHint: "0 = read as soon as you enter (the dot clears at once). Above 0 you must stay in that session for this many seconds before it counts as read: leaving it resets the clock, and so does a reload. The clock only ever covers **the ending that was already there when you entered** — a turn that finishes while you are sitting in the session does not count: it stays red until you leave and come back. Capped at 600 seconds.
|
|
156
|
+
clearDelayHint: "0 = read as soon as you enter (the dot clears at once). Above 0 you must stay in that session for this many seconds before it counts as read: leaving it resets the clock, and so does a reload. The clock only ever covers **the ending that was already there when you entered** — a turn that finishes while you are sitting in the session does not count: it stays red until you leave and come back. Capped at 600 seconds. A settings or plugins page, or a tab in the background, only **pauses** the clock — neither clears a dot (neither counts as leaving); the one thing that does count as leaving is **opening another session**. Read state lives on the Host ($DSH_HOME/custom-favicon/unread-seen.json): read on any device means read on every device, and the browser's own copy is only a cache — clearing site data no longer throws it away."
|
|
157
157
|
};
|
|
158
158
|
//#endregion
|
|
159
159
|
|
|
@@ -613,10 +613,25 @@ window.__ModuleLoader__.load({
|
|
|
613
613
|
* @param view - the screen report described above (may be absent).
|
|
614
614
|
* @returns the ids being viewed, in list order; `[]` when there are none.
|
|
615
615
|
*/
|
|
616
|
+
/**
|
|
617
|
+
* Which session this page is ON — the set that defines "entered" and "left".
|
|
618
|
+
*
|
|
619
|
+
* Only a change here counts as leaving a session, so this must NOT be emptied
|
|
620
|
+
* by pausing: a background tab, or a global panel (settings, plugins, …) that
|
|
621
|
+
* covers the conversation, keeps you on the same session. Emptying it would
|
|
622
|
+
* drop the entered-at snapshot, and taking a fresh one on return is exactly
|
|
623
|
+
* what acknowledges an ending you never left the session for — the bug behind
|
|
624
|
+
* "切一下标签页红点就没了".
|
|
625
|
+
*
|
|
626
|
+
* `view.displayed` (read from the DOM) is the precise answer when available;
|
|
627
|
+
* `list.current` and then retention are the fallbacks, which is also what keeps
|
|
628
|
+
* the answer meaningful while a panel covers the conversation.
|
|
629
|
+
* @param list - the session list snapshot (may be absent).
|
|
630
|
+
* @param view - `{ displayed }`: the session the conversation is rendering.
|
|
631
|
+
* @returns the ids being viewed, in list order; `[]` when there are none.
|
|
632
|
+
*/
|
|
616
633
|
function currentSessionIds(list, view) {
|
|
617
634
|
const screen = view !== null && typeof view === "object" ? view : {};
|
|
618
|
-
if (screen.visible === false) return [];
|
|
619
|
-
if (screen.panelActive === true) return [];
|
|
620
635
|
if (typeof screen.displayed === "string" && screen.displayed !== "") return [screen.displayed];
|
|
621
636
|
if (list === null || typeof list !== "object") return [];
|
|
622
637
|
if (typeof list.current === "string" && list.current !== "") return [list.current];
|
|
@@ -1120,15 +1135,45 @@ window.__ModuleLoader__.load({
|
|
|
1120
1135
|
// Never silent: a failure here is invisible in the UI (the tab simply
|
|
1121
1136
|
// keeps its previous icon), which once cost a whole debugging session.
|
|
1122
1137
|
// Warn once per distinct reason so the 5s watchdog cannot spam.
|
|
1123
|
-
|
|
1124
|
-
if (why !== lastComposeError) {
|
|
1125
|
-
lastComposeError = why;
|
|
1126
|
-
try { console.warn("dsh-icon-custom: 标签页红点合成失败 / favicon badge compose failed:", why); } catch {}
|
|
1127
|
-
}
|
|
1138
|
+
warnFaviconCompose(error);
|
|
1128
1139
|
} finally {
|
|
1129
1140
|
if (seq === composeSeq) faviconComposing = false;
|
|
1130
1141
|
}
|
|
1131
1142
|
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Report a failed tab-icon composition, once per distinct reason.
|
|
1145
|
+
*
|
|
1146
|
+
* The 5-second watchdog retries, so repeating the same line would bury the
|
|
1147
|
+
* console in noise; a NEW reason is exactly the signal worth seeing.
|
|
1148
|
+
* @param error - whatever the compose threw.
|
|
1149
|
+
*/
|
|
1150
|
+
function warnFaviconCompose(error) {
|
|
1151
|
+
const why = String((error && error.message) || error);
|
|
1152
|
+
if (why === lastComposeError) return;
|
|
1153
|
+
lastComposeError = why;
|
|
1154
|
+
try { console.warn("dsh-icon-custom: 标签页红点合成失败 / favicon badge compose failed:", why); } catch {}
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Fire-and-forget composition whose failures are still reported.
|
|
1158
|
+
*
|
|
1159
|
+
* `applyBadgeToFavicon` is async, so it never throws synchronously: a caller's
|
|
1160
|
+
* `try { ... } catch {}` (the badge listeners all have one) cannot see its
|
|
1161
|
+
* rejection, and the tab icon would simply never change with nothing in the
|
|
1162
|
+
* console — the exact "no warning, no badge" state. Every call site goes through
|
|
1163
|
+
* here instead.
|
|
1164
|
+
* @param count - sessions needing you; 0 restores the plain icon.
|
|
1165
|
+
* @param size - badge size key.
|
|
1166
|
+
*/
|
|
1167
|
+
function composeFavicon(count, size) {
|
|
1168
|
+
try {
|
|
1169
|
+
Promise.resolve(applyBadgeToFavicon(count, size)).catch(warnFaviconCompose);
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
// An `async` callee never throws synchronously, so this arm is dead TODAY —
|
|
1172
|
+
// it is here so a future refactor (an early-returning sync path) cannot
|
|
1173
|
+
// quietly reintroduce the silent "no warning, no badge" state.
|
|
1174
|
+
warnFaviconCompose(error);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1132
1177
|
/**
|
|
1133
1178
|
* Re-assert the tab badge after anything outside this plugin disturbed the
|
|
1134
1179
|
* icon link — a cached page, a browser extension, a tab move, or a plugin
|
|
@@ -1145,17 +1190,17 @@ window.__ModuleLoader__.load({
|
|
|
1145
1190
|
// Carries the custom icon, or puts the shipped pair back. Both writers
|
|
1146
1191
|
// only touch a link that actually differs, so this stays a no-op once
|
|
1147
1192
|
// the page is correct and cannot re-arm the head observer.
|
|
1148
|
-
|
|
1193
|
+
composeFavicon(0);
|
|
1149
1194
|
return;
|
|
1150
1195
|
}
|
|
1151
1196
|
if (ours || faviconComposing) return; // already showing, or a build is running
|
|
1152
|
-
|
|
1197
|
+
composeFavicon(effectiveCount(), badgeSize);
|
|
1153
1198
|
}
|
|
1154
1199
|
// A freshly applied base icon replaces whatever the browser holds, so the
|
|
1155
1200
|
// composite must be rebuilt from the NEW base instead of the stale one.
|
|
1156
1201
|
function refreshFaviconBase() {
|
|
1157
1202
|
faviconBase = null;
|
|
1158
|
-
|
|
1203
|
+
composeFavicon(effectiveCount(), badgeSize);
|
|
1159
1204
|
}
|
|
1160
1205
|
//#endregion
|
|
1161
1206
|
|
|
@@ -1732,6 +1777,9 @@ window.__ModuleLoader__.load({
|
|
|
1732
1777
|
// conversation (its own standard prop, the same one the shipped sidebar reads)
|
|
1733
1778
|
// and the tab going to the background. Both are read here, not inferred, so
|
|
1734
1779
|
// "you are looking at it" stops being a guess about stale retention.
|
|
1780
|
+
// Pausing signals, NOT leaving signals: they stop the stay clock without
|
|
1781
|
+
// touching which session this page is on (so the entered-at snapshot
|
|
1782
|
+
// survives). Only switching to another session counts as leaving.
|
|
1735
1783
|
const panelActive = typeof props.usePanelInfo === "function"
|
|
1736
1784
|
? props.usePanelInfo((info) => info !== null && typeof info === "object" && info.activePanelId !== null)
|
|
1737
1785
|
: false;
|
|
@@ -1741,7 +1789,9 @@ window.__ModuleLoader__.load({
|
|
|
1741
1789
|
window.addEventListener("visibilitychange", onVisibility);
|
|
1742
1790
|
return () => window.removeEventListener("visibilitychange", onVisibility);
|
|
1743
1791
|
}, []);
|
|
1744
|
-
|
|
1792
|
+
/** Looking at the conversation right now: on screen, and this tab in front. */
|
|
1793
|
+
const watching = visible && panelActive !== true;
|
|
1794
|
+
const currentIds = currentSessionIds(list, { displayed: displayedSessionId() });
|
|
1745
1795
|
const items = collectUnread(list, pending, config, seenState.seen);
|
|
1746
1796
|
const count = items.length;
|
|
1747
1797
|
// Being in a session is what marks it read — and the clock that does it is
|
|
@@ -1778,8 +1828,10 @@ window.__ModuleLoader__.load({
|
|
|
1778
1828
|
/**
|
|
1779
1829
|
* Entered-at snapshot: `id → endAt` as it stood when that session joined
|
|
1780
1830
|
* the view. Deliberately NOT refreshed while the session stays — that is
|
|
1781
|
-
* the rule ("an ending during your stay waits for the next entry").
|
|
1782
|
-
* session that
|
|
1831
|
+
* the rule ("an ending during your stay waits for the next entry"). Only a
|
|
1832
|
+
* session that LEAVES the view is forgotten, so coming back to it takes a
|
|
1833
|
+
* fresh snapshot; pausing (background tab, a global panel) keeps both the
|
|
1834
|
+
* session and its snapshot.
|
|
1783
1835
|
*/
|
|
1784
1836
|
const stayArmed = React.useRef(new Map());
|
|
1785
1837
|
React.useEffect(() => {
|
|
@@ -1791,7 +1843,10 @@ window.__ModuleLoader__.load({
|
|
|
1791
1843
|
for (const id of stayIds) {
|
|
1792
1844
|
if (!stayArmed.current.has(id)) stayArmed.current.set(id, lastTurnEndAt(snapshot[id]));
|
|
1793
1845
|
}
|
|
1794
|
-
|
|
1846
|
+
// Paused: nothing may be acknowledged while nobody is watching, but the
|
|
1847
|
+
// snapshots above stay put — that is what makes resuming continue the
|
|
1848
|
+
// same entry instead of silently re-entering it.
|
|
1849
|
+
if (staySignature === "" || watching !== true) return undefined;
|
|
1795
1850
|
const armed = stayIds.map((id) => ({ id, endAt: stayArmed.current.get(id) }));
|
|
1796
1851
|
let cancelled = false;
|
|
1797
1852
|
let timer = 0;
|
|
@@ -1812,7 +1867,7 @@ window.__ModuleLoader__.load({
|
|
|
1812
1867
|
cancelled = true;
|
|
1813
1868
|
if (timer !== 0) window.clearTimeout(timer);
|
|
1814
1869
|
};
|
|
1815
|
-
}, [staySignature, delayMs]);
|
|
1870
|
+
}, [staySignature, delayMs, watching]);
|
|
1816
1871
|
React.useEffect(() => { emitRealBadge(count, items); }, [count, items]);
|
|
1817
1872
|
// Map session → its Workspace from the official snapshot: the ID anchors the
|
|
1818
1873
|
// folder dot to the keyed row, the title is what the row displays (not always
|
|
@@ -2794,15 +2849,15 @@ window.__ModuleLoader__.load({
|
|
|
2794
2849
|
// this effect keeps the tab icon in sync. Teardown restores the
|
|
2795
2850
|
// pristine platform/custom icon.
|
|
2796
2851
|
ctx.effect(() => {
|
|
2797
|
-
const unsubscribe = subscribeBadge((snapshot) => {
|
|
2852
|
+
const unsubscribe = subscribeBadge((snapshot) => { composeFavicon(snapshot.count, snapshot.size); });
|
|
2798
2853
|
// Level-triggered on purpose: (re)applying this half must adopt the
|
|
2799
2854
|
// value that is already on the bridge. The brand mark reads it at
|
|
2800
2855
|
// mount; the tab icon has to be told, or a plugin hot-reload would
|
|
2801
2856
|
// leave it restored (the old effect's cleanup) until the next change.
|
|
2802
|
-
|
|
2857
|
+
composeFavicon(effectiveCount(), badgeSize);
|
|
2803
2858
|
return () => {
|
|
2804
2859
|
unsubscribe();
|
|
2805
|
-
|
|
2860
|
+
composeFavicon(0);
|
|
2806
2861
|
};
|
|
2807
2862
|
}, "dsh-icon-custom: favicon badge");
|
|
2808
2863
|
// Watchdog. The tab icon is the one surface other parties can disturb
|
|
@@ -2879,7 +2934,7 @@ window.__ModuleLoader__.load({
|
|
|
2879
2934
|
}, "dsh-icon-custom: workspace dots");
|
|
2880
2935
|
// The platform favicon.svg repaints itself for dark mode through
|
|
2881
2936
|
// prefers-color-scheme; a composited bitmap cannot, so rebuild it.
|
|
2882
|
-
ctx.on("theme/change", () => {
|
|
2937
|
+
ctx.on("theme/change", () => { composeFavicon(effectiveCount(), badgeSize); });
|
|
2883
2938
|
}
|
|
2884
2939
|
exports.apply = apply;
|
|
2885
2940
|
exports.inject = inject;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cowwo/dsh-icon-custom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "自定义浏览器标签页图标:在设置页上传 SVG/PNG/ICO,立即生效并持久保存,自动适配安卓/iOS/平板 PWA 图标,并可把未读数字显示为系统应用图标角标(Windows 任务栏)。· Custom browser tab favicon: upload SVG/PNG/ICO from the settings page, applies instantly and persists, auto-sizes PWA icons for Android/iOS/tablets, and projects the unread count onto the system app-icon badge (Windows taskbar).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|