@cowwo/dsh-icon-custom 0.16.0 → 0.19.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.md +24 -7
- package/README.zh.md +33 -8
- package/client/client.js +1189 -107
- package/lib/index.js +52 -0
- package/lib/running.js +32 -0
- package/lib/typert.host.js +78 -3
- package/lib/unread.js +18 -4
- package/package.json +3 -3
package/lib/index.js
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
normalizeUnreadConfig,
|
|
41
41
|
pruneSeen
|
|
42
42
|
} from './unread.js'
|
|
43
|
+
import { normalizeRunningConfig } from './running.js'
|
|
43
44
|
|
|
44
45
|
const MAX_BYTES = 10 * 1024 * 1024 // 10 MB
|
|
45
46
|
// Normalized mime -> safe extension. CUR (mouse cursor) is accepted as ICO.
|
|
@@ -338,6 +339,11 @@ export class FaviconCustomService extends TypertRemoteService {
|
|
|
338
339
|
// survives a cleared browser profile and means the same thing on every
|
|
339
340
|
// device, while each browser keeps its own copy as cache + offline queue.
|
|
340
341
|
this.seenPath = join(this.storageDir, 'unread-seen.json')
|
|
342
|
+
// The "show a yellow mark for every session that is running" switch. Its own
|
|
343
|
+
// file and its own endpoint pair: the running FACT is official (the browser
|
|
344
|
+
// list already carries each session's `running`), so nothing about it is
|
|
345
|
+
// stored here — only whether to show the projection of it.
|
|
346
|
+
this.runningPath = join(this.storageDir, 'running.json')
|
|
341
347
|
// { activeId, icons: { [id]: { id, mime, ext, name, size, savedAt, rev, pwa, logo } } }
|
|
342
348
|
this.library = { activeId: null, icons: {} }
|
|
343
349
|
// The active icon's loaded state: { rev, mime, ext, name, size, savedAt, pwa, logo, bytes } | null
|
|
@@ -346,12 +352,16 @@ export class FaviconCustomService extends TypertRemoteService {
|
|
|
346
352
|
// Kept in its own file so a settings change can never corrupt the icon
|
|
347
353
|
// library index.
|
|
348
354
|
this.unread = normalizeUnreadConfig(null)
|
|
355
|
+
// Running-mark switch: whether the yellow projection of the official
|
|
356
|
+
// `running` fact is drawn. See lib/running.js.
|
|
357
|
+
this.running = normalizeRunningConfig(null)
|
|
349
358
|
// Host-side watermark: { [sessionId]: endAt } — see lib/unread.js.
|
|
350
359
|
this.seen = {}
|
|
351
360
|
// Platform PWA manifest (raw + parsed), read once at startup.
|
|
352
361
|
this.baseManifest = loadBaseManifest()
|
|
353
362
|
this._load()
|
|
354
363
|
this._loadUnread()
|
|
364
|
+
this._loadRunning()
|
|
355
365
|
this._loadUnreadSeen()
|
|
356
366
|
// "How did this session's last turn end" as an official session
|
|
357
367
|
// projection. Optional registration on purpose: this plugin is a display
|
|
@@ -450,6 +460,19 @@ export class FaviconCustomService extends TypertRemoteService {
|
|
|
450
460
|
return writeFile(this.unreadPath, JSON.stringify(this.unread, null, 2))
|
|
451
461
|
}
|
|
452
462
|
|
|
463
|
+
/** Load the running-mark switch (missing or corrupt file = default: on). */
|
|
464
|
+
_loadRunning() {
|
|
465
|
+
try {
|
|
466
|
+
const raw = JSON.parse(readFileSync(this.runningPath).toString('utf8'))
|
|
467
|
+
this.running = normalizeRunningConfig(raw)
|
|
468
|
+
} catch { /* no persisted switch yet: keep the default */ }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Persist the running-mark switch (best effort, advisory file). */
|
|
472
|
+
_saveRunning() {
|
|
473
|
+
return writeFile(this.runningPath, JSON.stringify(this.running, null, 2))
|
|
474
|
+
}
|
|
475
|
+
|
|
453
476
|
/**
|
|
454
477
|
* Load the host-side watermark.
|
|
455
478
|
*
|
|
@@ -516,6 +539,35 @@ export class FaviconCustomService extends TypertRemoteService {
|
|
|
516
539
|
return this._unreadRuleView()
|
|
517
540
|
}
|
|
518
541
|
|
|
542
|
+
/**
|
|
543
|
+
* typert endpoint: read the running-mark switch.
|
|
544
|
+
*
|
|
545
|
+
* Its own endpoint pair, not a field on the unread rule: the two features are
|
|
546
|
+
* independent (a red dot says "something ended and you have not looked", a
|
|
547
|
+
* yellow mark says "something is running right now"), and neither switch may
|
|
548
|
+
* drag the other's storage or response shape along. There is deliberately no
|
|
549
|
+
* `running` field here — who is running is the official host's answer and
|
|
550
|
+
* travels in the ordinary session list.
|
|
551
|
+
* @returns `{ enabled }`.
|
|
552
|
+
*/
|
|
553
|
+
async getRunningRule() {
|
|
554
|
+
return { ...this.running }
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* typert endpoint: flip the running-mark switch.
|
|
559
|
+
* @param request - `{ enabled: boolean }`; anything else falls back to the default.
|
|
560
|
+
* @returns the stored switch.
|
|
561
|
+
*/
|
|
562
|
+
async setRunningRule(request) {
|
|
563
|
+
if (request === null || typeof request !== 'object') {
|
|
564
|
+
throw new Error('缺少 enabled 参数')
|
|
565
|
+
}
|
|
566
|
+
this.running = normalizeRunningConfig(request)
|
|
567
|
+
await this._saveRunning().catch(() => {}) // switch file is advisory
|
|
568
|
+
return { ...this.running }
|
|
569
|
+
}
|
|
570
|
+
|
|
519
571
|
/**
|
|
520
572
|
* typert endpoint: merge this browser's "seen" watermarks into the Host's copy.
|
|
521
573
|
*
|
package/lib/running.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-icon-custom · 「进行中黄点」的开关(宿主半,纯函数)
|
|
3
|
+
*
|
|
4
|
+
* 这里**没有**任何"谁在跑"的事实,一条也没有——那是官方的:
|
|
5
|
+
* 宿主 `ctx.agents.get(id)?.status === 'running'` 经 `api-session/status`
|
|
6
|
+
* 事件实时推到浏览器,客户端会话列表里本来就带着每个会话的 `running`。
|
|
7
|
+
* 这个插件只把那件事**多投影几次**(黄点/黄数字/黄 favicon 角标)。
|
|
8
|
+
*
|
|
9
|
+
* 所以这个文件只有一个布尔开关,和一条纪律:
|
|
10
|
+
* **不落盘"进行中",只落盘"要不要显示"。** 进程重启后冷会话按官方口径
|
|
11
|
+
* 永远是 `running:false`,不会留下一个永远亮着的假黄点;而开关是偏好,
|
|
12
|
+
* 应该跨设备、跨重启跟着你,所以它归宿主,和红点规则并列但互不牵连
|
|
13
|
+
* (见 docs/adr/0010-running-mark-is-a-second-projection.md)。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** 默认:显示黄点。 */
|
|
17
|
+
export const RUNNING_MARK_DEFAULT = true
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 把任意输入夹成 `{ enabled }`。
|
|
21
|
+
*
|
|
22
|
+
* 只认严格的 `true`/`false`;缺项、脏值、null 一律回落默认——一个读不懂的
|
|
23
|
+
* 开关值不该把一整套标记的可见性变成猜谜。
|
|
24
|
+
* @param input - `{ enabled? }`,可以是脏的、缺项的、null
|
|
25
|
+
* @returns `{ enabled }` —— 永远完整
|
|
26
|
+
*/
|
|
27
|
+
export function normalizeRunningConfig(input) {
|
|
28
|
+
const source = input !== null && typeof input === 'object' ? input : {}
|
|
29
|
+
return {
|
|
30
|
+
enabled: typeof source.enabled === 'boolean' ? source.enabled : RUNNING_MARK_DEFAULT
|
|
31
|
+
}
|
|
32
|
+
}
|
package/lib/typert.host.js
CHANGED
|
@@ -57,6 +57,8 @@ const unreadRuleSchema = z.object({
|
|
|
57
57
|
appBadge: z.boolean(),
|
|
58
58
|
/** 进入会话后要连续待满多少秒才算"看过";0 = 进入即已读。上限 10 分钟。 */
|
|
59
59
|
clearDelaySec: z.number().int().nonnegative().max(600),
|
|
60
|
+
/** 实验项:点清单条目时在侧栏定位到该会话(展开工作区 + 滚动 + 闪一下;贴产品 DOM,可一键关闭)。 */
|
|
61
|
+
revealOnOpen: z.boolean(),
|
|
60
62
|
/**
|
|
61
63
|
* 宿主此刻的 `Date.now()`。
|
|
62
64
|
* 客户端用它把"看过"水位换算进宿主时钟域(`turn/end.time` 也是宿主时钟):
|
|
@@ -77,7 +79,8 @@ const setUnreadRuleRequestSchema = z.object({
|
|
|
77
79
|
pending: z.boolean().optional(),
|
|
78
80
|
workspaceDot: z.boolean().optional(),
|
|
79
81
|
appBadge: z.boolean().optional(),
|
|
80
|
-
clearDelaySec: z.number().int().nonnegative().max(600).optional()
|
|
82
|
+
clearDelaySec: z.number().int().nonnegative().max(600).optional(),
|
|
83
|
+
revealOnOpen: z.boolean().optional()
|
|
81
84
|
})
|
|
82
85
|
|
|
83
86
|
/** 一次水位线同步:浏览器把自己那份交上来,宿主逐会话取 max 后回权威表。 */
|
|
@@ -89,6 +92,21 @@ const unreadSeenSchema = z.object({
|
|
|
89
92
|
seen: z.record(z.string(), z.number())
|
|
90
93
|
})
|
|
91
94
|
|
|
95
|
+
/**
|
|
96
|
+
* 进行中黄点的开关。
|
|
97
|
+
*
|
|
98
|
+
* 这里**没有**"谁在跑"的字段,一条都没有:那是官方的 `SessionSummary.running`,
|
|
99
|
+
* 由官方宿主实时推给浏览器,插件只读不存。宿主这一侧只有"要不要显示"这一个偏好,
|
|
100
|
+
* 和红点规则并列、互不牵连(自己的端点对、自己的文件)。
|
|
101
|
+
*/
|
|
102
|
+
const runningRuleSchema = z.object({
|
|
103
|
+
enabled: z.boolean()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const setRunningRuleRequestSchema = z.object({
|
|
107
|
+
enabled: z.boolean().optional()
|
|
108
|
+
})
|
|
109
|
+
|
|
92
110
|
const setIconResultSchema = statusSchema
|
|
93
111
|
|
|
94
112
|
export const TYPERT = {
|
|
@@ -238,6 +256,45 @@ export const TYPERT = {
|
|
|
238
256
|
},
|
|
239
257
|
sourceLocation: { file: 'lib/index.js', line: 1, column: 1 }
|
|
240
258
|
},
|
|
259
|
+
{
|
|
260
|
+
id: 'dsh-icon-custom#iconCustom/getRunningRule',
|
|
261
|
+
service: 'iconCustom',
|
|
262
|
+
namespace: 'iconCustom',
|
|
263
|
+
method: 'getRunningRule',
|
|
264
|
+
invocation: { kind: 'direct' },
|
|
265
|
+
parameters: [],
|
|
266
|
+
result: {
|
|
267
|
+
mode: 'strict',
|
|
268
|
+
typeSymbol: 'dsh-icon-custom#iconCustom/getRunningRule:result',
|
|
269
|
+
create: () => runningRuleSchema
|
|
270
|
+
},
|
|
271
|
+
sourceLocation: { file: 'lib/index.js', line: 1, column: 1 }
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id: 'dsh-icon-custom#iconCustom/setRunningRule',
|
|
275
|
+
service: 'iconCustom',
|
|
276
|
+
namespace: 'iconCustom',
|
|
277
|
+
method: 'setRunningRule',
|
|
278
|
+
invocation: { kind: 'direct' },
|
|
279
|
+
parameters: [
|
|
280
|
+
{
|
|
281
|
+
name: 'request',
|
|
282
|
+
wire: 'request',
|
|
283
|
+
source: 'json',
|
|
284
|
+
codec: {
|
|
285
|
+
mode: 'strict',
|
|
286
|
+
typeSymbol: 'dsh-icon-custom#iconCustom/setRunningRule:request',
|
|
287
|
+
create: () => setRunningRuleRequestSchema
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
],
|
|
291
|
+
result: {
|
|
292
|
+
mode: 'strict',
|
|
293
|
+
typeSymbol: 'dsh-icon-custom#iconCustom/setRunningRule:result',
|
|
294
|
+
create: () => runningRuleSchema
|
|
295
|
+
},
|
|
296
|
+
sourceLocation: { file: 'lib/index.js', line: 1, column: 1 }
|
|
297
|
+
},
|
|
241
298
|
{
|
|
242
299
|
id: 'dsh-icon-custom#iconCustom/setUnreadSeen',
|
|
243
300
|
service: 'iconCustom',
|
|
@@ -322,7 +379,7 @@ export const TYPERT = {
|
|
|
322
379
|
{
|
|
323
380
|
kind: 'method',
|
|
324
381
|
name: 'getUnreadRule',
|
|
325
|
-
signature: 'async getUnreadRule(): Promise<{ reasons: Record<string, boolean>; pending: boolean; workspaceDot: boolean; appBadge: boolean; clearDelaySec: number; hostNow: number; seen: Record<string, number> }>',
|
|
382
|
+
signature: 'async getUnreadRule(): Promise<{ reasons: Record<string, boolean>; pending: boolean; workspaceDot: boolean; appBadge: boolean; clearDelaySec: number; revealOnOpen: boolean; hostNow: number; seen: Record<string, number> }>',
|
|
326
383
|
description: "Read the unread-badge rule, the Host clock and the seen watermark.",
|
|
327
384
|
summary: "Read the unread-badge rule.",
|
|
328
385
|
jsDoc: "/** Read the unread-badge rule. */",
|
|
@@ -331,7 +388,7 @@ export const TYPERT = {
|
|
|
331
388
|
{
|
|
332
389
|
kind: 'method',
|
|
333
390
|
name: 'setUnreadRule',
|
|
334
|
-
signature: 'async setUnreadRule(request: { reasons: Record<string, boolean>; pending?: boolean; workspaceDot?: boolean; appBadge?: boolean; clearDelaySec?: number }): Promise<{ reasons: Record<string, boolean>; pending: boolean; workspaceDot: boolean; appBadge: boolean; clearDelaySec: number; hostNow: number; seen: Record<string, number> }>',
|
|
391
|
+
signature: 'async setUnreadRule(request: { reasons: Record<string, boolean>; pending?: boolean; workspaceDot?: boolean; appBadge?: boolean; clearDelaySec?: number; revealOnOpen?: boolean }): Promise<{ reasons: Record<string, boolean>; pending: boolean; workspaceDot: boolean; appBadge: boolean; clearDelaySec: number; revealOnOpen: boolean; hostNow: number; seen: Record<string, number> }>',
|
|
335
392
|
description: "Replace the unread-badge rule: which turn-end reasons light the red dot.",
|
|
336
393
|
summary: "Set the unread-badge rule.",
|
|
337
394
|
jsDoc: "/** Set the unread-badge rule. */",
|
|
@@ -345,6 +402,24 @@ export const TYPERT = {
|
|
|
345
402
|
summary: "Merge the seen watermark.",
|
|
346
403
|
jsDoc: "/** Merge the seen watermark. */",
|
|
347
404
|
tags: []
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
kind: 'method',
|
|
408
|
+
name: 'getRunningRule',
|
|
409
|
+
signature: 'async getRunningRule(): Promise<{ enabled: boolean }>',
|
|
410
|
+
description: "Read the running-mark switch (yellow mark for sessions that are running right now).",
|
|
411
|
+
summary: "Read the running-mark switch.",
|
|
412
|
+
jsDoc: "/** Read the running-mark switch. */",
|
|
413
|
+
tags: []
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
kind: 'method',
|
|
417
|
+
name: 'setRunningRule',
|
|
418
|
+
signature: 'async setRunningRule(request: { enabled?: boolean }): Promise<{ enabled: boolean }>',
|
|
419
|
+
description: "Flip the running-mark switch. The running fact itself is official and is never stored here.",
|
|
420
|
+
summary: "Set the running-mark switch.",
|
|
421
|
+
jsDoc: "/** Set the running-mark switch. */",
|
|
422
|
+
tags: []
|
|
348
423
|
}
|
|
349
424
|
],
|
|
350
425
|
types: []
|
package/lib/unread.js
CHANGED
|
@@ -46,12 +46,25 @@ export const UNREAD_PENDING_DEFAULT = true
|
|
|
46
46
|
/**
|
|
47
47
|
* 默认:在工作区那一行的文件夹图标上画红点。
|
|
48
48
|
*
|
|
49
|
-
*
|
|
49
|
+
* 这是全套功能里**贴着产品 DOM 走**的两处之一(另一处见下面的 `UNREAD_REVEAL_DEFAULT`),
|
|
50
50
|
* 所以它有自己的开关:一旦 DSH 升级改了结构导致它失效或画歪,
|
|
51
51
|
* 关掉它就行,红点数字/清单/跳转那些稳的部分完全不受影响。
|
|
52
52
|
*/
|
|
53
53
|
export const UNREAD_WORKSPACE_DOT_DEFAULT = true
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* 默认:点清单条目时,顺带在侧栏定位到那个会话(展开它所在的工作区、滚到那一行并闪一下)。
|
|
57
|
+
*
|
|
58
|
+
* 这是第二处贴着产品 DOM 走的部分。官方的"展开 + 滚动"只服务于它自己的搜索结果
|
|
59
|
+
* (内部状态,没有对外的 Service),所以插件只能**模拟点击**官方的工作区行与
|
|
60
|
+
* "显示更多"按钮。DSH 升级改结构时它会失效,失效的表现只有一个:
|
|
61
|
+
* **点进去照常打开会话,只是侧栏不动**。不想让它动侧栏就关掉这一项。
|
|
62
|
+
*
|
|
63
|
+
* 还有一件事要说清:展开是**官方自己记住**的状态(存在浏览器里),不会自己收回去 ——
|
|
64
|
+
* 官方搜索跳转也是这个行为。
|
|
65
|
+
*/
|
|
66
|
+
export const UNREAD_REVEAL_DEFAULT = true
|
|
67
|
+
|
|
55
68
|
/**
|
|
56
69
|
* 默认:在系统应用图标上显示角标(安装为应用后生效)。
|
|
57
70
|
*
|
|
@@ -97,8 +110,8 @@ export function normalizeReasons(input) {
|
|
|
97
110
|
|
|
98
111
|
/**
|
|
99
112
|
* 把任意输入夹成完整设置。
|
|
100
|
-
* @param input - `{ reasons?, pending?, workspaceDot?, appBadge?, clearDelaySec? }`,可以是脏的、缺项的、null
|
|
101
|
-
* @returns `{ reasons, pending, workspaceDot, appBadge, clearDelaySec }` —— 每一项都完整
|
|
113
|
+
* @param input - `{ reasons?, pending?, workspaceDot?, appBadge?, clearDelaySec?, revealOnOpen? }`,可以是脏的、缺项的、null
|
|
114
|
+
* @returns `{ reasons, pending, workspaceDot, appBadge, clearDelaySec, revealOnOpen }` —— 每一项都完整
|
|
102
115
|
*/
|
|
103
116
|
export function normalizeUnreadConfig(input) {
|
|
104
117
|
const source = input !== null && typeof input === 'object' ? input : {}
|
|
@@ -107,7 +120,8 @@ export function normalizeUnreadConfig(input) {
|
|
|
107
120
|
pending: typeof source.pending === 'boolean' ? source.pending : UNREAD_PENDING_DEFAULT,
|
|
108
121
|
workspaceDot: typeof source.workspaceDot === 'boolean' ? source.workspaceDot : UNREAD_WORKSPACE_DOT_DEFAULT,
|
|
109
122
|
appBadge: typeof source.appBadge === 'boolean' ? source.appBadge : UNREAD_APP_BADGE_DEFAULT,
|
|
110
|
-
clearDelaySec: normalizeClearDelay(source.clearDelaySec)
|
|
123
|
+
clearDelaySec: normalizeClearDelay(source.clearDelaySec),
|
|
124
|
+
revealOnOpen: typeof source.revealOnOpen === 'boolean' ? source.revealOnOpen : UNREAD_REVEAL_DEFAULT
|
|
111
125
|
}
|
|
112
126
|
}
|
|
113
127
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cowwo/dsh-icon-custom",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "自定义浏览器标签页图标:在设置页上传 SVG/PNG/ICO,立即生效并持久保存,自动适配安卓/iOS/平板 PWA
|
|
3
|
+
"version": "0.19.0",
|
|
4
|
+
"description": "自定义浏览器标签页图标:在设置页上传 SVG/PNG/ICO,立即生效并持久保存,自动适配安卓/iOS/平板 PWA 图标,显示未读红点与进行中黄点(工作区数字/会话行/左上角/标签页,数字可点开清单、点条目定位到侧栏),并可把未读数显示为系统应用图标角标(Windows 任务栏 / macOS Dock,含通知权限申请)。· Custom browser tab favicon: upload SVG/PNG/ICO from the settings page, applies instantly and persists, auto-sizes PWA icons for Android/iOS/tablets, shows unread red dots and running yellow marks (workspace number / session row / top-left / tab icon, both counts open a list whose rows locate the session in the sidebar), and projects the unread count onto the system app-icon badge (Windows taskbar / macOS Dock, notifications permission included).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"scripts": {
|
|
9
|
-
"test": "node test/app-badge.mjs && node test/unread-panel.mjs && node test/unread-clear.mjs && node test/unread-rows.mjs && node test/unread-seen.mjs && node test/unread-host-sync.mjs"
|
|
9
|
+
"test": "node test/app-badge.mjs && node test/notification-permission.mjs && node test/unread-panel.mjs && node test/unread-clear.mjs && node test/unread-rows.mjs && node test/unread-seen.mjs && node test/unread-host-sync.mjs && node test/running-mark.mjs && node test/running-panel.mjs && node test/reveal.mjs"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"jimp": "^1.6.1",
|