@cowwo/dsh-icon-custom 0.2.1 → 0.5.1

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 CHANGED
@@ -29,6 +29,16 @@ When the site is installed as an app, the browser's install dialog picks the cus
29
29
 
30
30
  ![PWA install dialog](./docs/install-dialog.png)
31
31
 
32
+ ## Page logo mark
33
+
34
+ Tick **同时替换页面 Logo 图标** to also replace the whale mark at the top-left of the DSH page — the mark beside the `deepseek HARNESS` brand name. The brand **text is untouched**; the mark falls back to the official whale while the option is off. The change applies instantly and persists across reloads/restarts.
35
+
36
+ Notes:
37
+
38
+ - The option is **off by default**.
39
+ - It can be toggled at any time after upload — no re-upload needed.
40
+ - This only affects the sidebar brand mark (the one next to `deepseek HARNESS`); PWA/installed-app icons are a separate option above.
41
+
32
42
  Notes:
33
43
 
34
44
  - The option is **off by default**: without it, behavior is identical to before and the platform manifest is served verbatim.
package/README.zh.md CHANGED
@@ -29,6 +29,16 @@ dsh plugin --profile web add dsh-icon-custom
29
29
 
30
30
  ![PWA 安装对话框](./docs/install-dialog.png)
31
31
 
32
+ ## 页面 Logo 图标
33
+
34
+ 勾选**同时替换页面 Logo 图标**,网页左上角品牌 logo 里的**鲸鱼图标**就会换成你上传的这张——`deepseek HARNESS` 文字不受影响。选项关闭时显示官方鲸鱼;改动立即生效,刷新/重启都保留。
35
+
36
+ 说明:
37
+
38
+ - 该选项**默认不勾选**。
39
+ - 上传后随时可以勾选/取消,不用重新上传文件。
40
+ - 只影响侧边栏的品牌图标(deepseek HARNESS 旁边那只);PWA 安装图标是上面的另一个选项。
41
+
32
42
  说明:
33
43
 
34
44
  - 该选项**默认不勾选**:不勾选时行为与之前完全一致,平台 manifest 原样透传。
package/client/client.js CHANGED
@@ -28,6 +28,9 @@ window.__ModuleLoader__.load({
28
28
  pwa: "同时替换 PWA 安装图标",
29
29
  pwaHint: "安装为应用(添加到主屏幕)后,桌面上的图标也换成这张;已安装的应用可能需要重新安装才会更新。",
30
30
  successPwa: "PWA 图标设置已更新",
31
+ logo: "同时替换页面 Logo 图标",
32
+ logoHint: "把网页左上角品牌 logo 里的鲸鱼图标换成这张(不影响 deepseek HARNESS 文字)。",
33
+ successLogo: "页面 Logo 已更新",
31
34
  fail: "操作失败",
32
35
  noFile: "请先选择图标文件"
33
36
  };
@@ -50,6 +53,9 @@ window.__ModuleLoader__.load({
50
53
  pwa: "Also replace the PWA app icon",
51
54
  pwaHint: "The icon used after installing the site as an app (add to home screen). Already-installed apps may need a reinstall to pick it up.",
52
55
  successPwa: "PWA icon setting updated",
56
+ logo: "Also replace the page logo mark",
57
+ logoHint: "Replaces the whale mark beside the brand name in the top-left (the deepseek HARNESS text stays).",
58
+ successLogo: "Page logo updated",
53
59
  fail: "Operation failed",
54
60
  noFile: "Choose a file first"
55
61
  };
@@ -72,14 +78,60 @@ window.__ModuleLoader__.load({
72
78
  return new Date(ms).toLocaleString();
73
79
  } catch { return "—"; }
74
80
  }
81
+ // In-package brand-mark bridge: the settings section pushes the current
82
+ // logo state here; the sidebar brand-mark component subscribes to it.
83
+ // Both live in this same module — no RPC loop, no cross-package events.
84
+ const logoListeners = new Set();
85
+ function emitLogo(value) {
86
+ logoListeners.forEach((fn) => { try { fn(value); } catch {} });
87
+ }
88
+ function subscribeLogo(fn) {
89
+ logoListeners.add(fn);
90
+ return () => { logoListeners.delete(fn); };
91
+ }
92
+ function syncLogo(status) {
93
+ emitLogo({
94
+ enabled: status !== null && typeof status === "object" && status.active === true && status.logo === true,
95
+ rev: status && status.rev ? status.rev : null
96
+ });
97
+ }
75
98
  //#endregion
76
99
 
77
- //#region component
100
+ //#region components
101
+ /**
102
+ * Sidebar brand-mark occupant (registered on the official
103
+ * `sidebar.brand.mark` single seat). Shows the custom icon when the
104
+ * logo option is on; otherwise falls back to the platform favicon mark
105
+ * (/favicon.svg is the same official whale). The brand NAME text is not
106
+ * touched — this seat is only the mark.
107
+ */
108
+ function BrandMark(props) {
109
+ const [state, setState] = React.useState(null);
110
+ React.useEffect(() => {
111
+ const unsubscribe = subscribeLogo(setState);
112
+ // Pull the current state on mount too, so the mark is correct
113
+ // even when the settings panel was never opened this session.
114
+ props.rpc.call("/api", "iconCustom/getStatus", { args: {} })
115
+ .then((resp) => { if (resp && resp.ok === true) syncLogo(resp.value); })
116
+ .catch(() => {});
117
+ return unsubscribe;
118
+ }, [props.rpc]);
119
+ const size = (props && props.size) || 24;
120
+ const custom = state && state.enabled === true && state.rev;
121
+ const style = { width: size, height: size, objectFit: "contain", display: "block" };
122
+ return React.createElement("img", {
123
+ src: custom ? `/icon-custom.svg?v=${state.rev}` : "/favicon.svg",
124
+ alt: "",
125
+ style
126
+ });
127
+ }
128
+
78
129
  function FaviconSection(props) {
79
130
  const t = props.t;
80
131
  const rpc = props.rpc;
81
132
  const [status, setStatus] = React.useState(null);
82
133
  const [pwaEnabled, setPwaEnabled] = React.useState(false);
134
+ const [logoEnabled, setLogoEnabled] = React.useState(false);
83
135
  const [busy, setBusy] = React.useState(false);
84
136
  const [error, setError] = React.useState("");
85
137
  const [notice, setNotice] = React.useState("");
@@ -91,6 +143,8 @@ window.__ModuleLoader__.load({
91
143
  if (resp && resp.ok === true) {
92
144
  setStatus(resp.value);
93
145
  setPwaEnabled(resp.value.pwa === true);
146
+ setLogoEnabled(resp.value.logo === true);
147
+ syncLogo(resp.value);
94
148
  } else setError(reason(resp, t("fail")));
95
149
  } catch (e) {
96
150
  setError(String((e && e.message) || e || t("fail")));
@@ -147,13 +201,15 @@ window.__ModuleLoader__.load({
147
201
  setBusy(true);
148
202
  try {
149
203
  const resp = await rpc.call("/api", "iconCustom/setIcon", {
150
- args: { request: { mime: file.type || "image/svg+xml", name: file.name, pwa: pwaEnabled, data } }
204
+ args: { request: { mime: file.type || "image/svg+xml", name: file.name, pwa: pwaEnabled, logo: logoEnabled, data } }
151
205
  });
152
206
  if (resp && resp.ok === true) {
153
207
  setStatus(resp.value);
154
208
  setPwaEnabled(resp.value.pwa === true);
209
+ setLogoEnabled(resp.value.logo === true);
155
210
  setNotice(t("successUpload"));
156
211
  applyLive(resp.value);
212
+ syncLogo(resp.value);
157
213
  } else {
158
214
  setError(reason(resp, t("fail")));
159
215
  }
@@ -165,7 +221,7 @@ window.__ModuleLoader__.load({
165
221
  };
166
222
  reader.onerror = () => setError(t("fail"));
167
223
  reader.readAsDataURL(file);
168
- }, [rpc, t, pwaEnabled, applyLive]);
224
+ }, [rpc, t, pwaEnabled, logoEnabled, applyLive]);
169
225
 
170
226
  const onReset = React.useCallback(async () => {
171
227
  setError("");
@@ -176,8 +232,10 @@ window.__ModuleLoader__.load({
176
232
  if (resp && resp.ok === true) {
177
233
  setStatus(resp.value);
178
234
  setPwaEnabled(false);
235
+ setLogoEnabled(false);
179
236
  setNotice(t("successReset"));
180
237
  applyDefault();
238
+ syncLogo(resp.value);
181
239
  } else {
182
240
  setError(reason(resp, t("fail")));
183
241
  }
@@ -214,6 +272,32 @@ window.__ModuleLoader__.load({
214
272
  }
215
273
  }, [rpc, t, applyLive]);
216
274
 
275
+ const onToggleLogo = React.useCallback(async (event) => {
276
+ const enabled = event.target.checked === true;
277
+ setError("");
278
+ setNotice("");
279
+ setBusy(true);
280
+ try {
281
+ const resp = await rpc.call("/api", "iconCustom/setLogo", {
282
+ args: { request: { enabled } }
283
+ });
284
+ if (resp && resp.ok === true) {
285
+ setStatus(resp.value);
286
+ setLogoEnabled(resp.value.logo === true);
287
+ setNotice(t("successLogo"));
288
+ syncLogo(resp.value);
289
+ } else {
290
+ setLogoEnabled(!enabled);
291
+ setError(reason(resp, t("fail")));
292
+ }
293
+ } catch (e) {
294
+ setLogoEnabled(!enabled);
295
+ setError(String((e && e.message) || e || t("fail")));
296
+ } finally {
297
+ setBusy(false);
298
+ }
299
+ }, [rpc, t]);
300
+
217
301
  const active = status && status.active === true;
218
302
  const previewSrc = active ? `/icon-custom.svg?v=${status.rev}` : "/favicon.svg";
219
303
 
@@ -258,6 +342,11 @@ window.__ModuleLoader__.load({
258
342
  React.createElement("span", null, t("pwa"))
259
343
  ),
260
344
  React.createElement("div", { style: style.hint }, t("pwaHint")),
345
+ React.createElement("label", { style: { display: "flex", alignItems: "center", gap: "8px", padding: "10px 0 0", fontSize: 12, color: "var(--dsw-alias-label-primary)", cursor: busy ? "default" : "pointer" } },
346
+ React.createElement("input", { type: "checkbox", checked: logoEnabled, disabled: busy || !active, onChange: onToggleLogo, style: { cursor: busy ? "default" : "pointer" } }),
347
+ React.createElement("span", null, t("logo"))
348
+ ),
349
+ React.createElement("div", { style: style.hint }, t("logoHint")),
261
350
  React.createElement("div", { style: style.hint }, t("uploadHint")),
262
351
  React.createElement("div", { style: style.hint }, t("resetHint")),
263
352
  notice ? React.createElement("div", { style: style.notice }, notice) : null,
@@ -271,6 +360,14 @@ window.__ModuleLoader__.load({
271
360
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-icon-custom: dictionaries");
272
361
  const t = ctx.locale.bind(NS);
273
362
  const rpc = ctx.connection.rpc;
363
+ // Brand-mark seat: replaces only the whale mark (verified: a
364
+ // third-party registration wins over the official occupant and the
365
+ // official package's mark steps aside). The brand NAME text is a
366
+ // separate seat and is left untouched. priority -1 is required:
367
+ // the official occupant holds priority 0, and same-priority
368
+ // registrations throw ("register at a different priority to shadow
369
+ // it (lowest renders)").
370
+ ctx.slots.inject("sidebar.brand.mark", () => ctx.slots.register({ name: "sidebar.brand.mark", priority: -1 }, (props) => React.createElement(BrandMark, { ...props, rpc })));
274
371
  ctx.slots.inject("settings.section", () => ctx.slots.register({
275
372
  name: "settings.section",
276
373
  id: "favicon",
package/lib/index.js CHANGED
@@ -218,9 +218,9 @@ export class FaviconCustomService extends TypertRemoteService {
218
218
  this.storageDir = join(resolveHome(), 'custom-favicon')
219
219
  this.libraryDir = join(this.storageDir, 'library')
220
220
  this.indexPath = join(this.storageDir, 'library.json')
221
- // { activeId, icons: { [id]: { id, mime, ext, name, size, savedAt, rev, pwa } } }
221
+ // { activeId, icons: { [id]: { id, mime, ext, name, size, savedAt, rev, pwa, logo } } }
222
222
  this.library = { activeId: null, icons: {} }
223
- // The active icon's loaded state: { rev, mime, ext, name, size, savedAt, pwa, bytes } | null
223
+ // The active icon's loaded state: { rev, mime, ext, name, size, savedAt, pwa, logo, bytes } | null
224
224
  this.current = null
225
225
  // Platform PWA manifest (raw + parsed), read once at startup.
226
226
  this.baseManifest = loadBaseManifest()
@@ -268,8 +268,8 @@ export class FaviconCustomService extends TypertRemoteService {
268
268
  status() {
269
269
  const c = this.current
270
270
  return c === null
271
- ? { active: false, pwa: false, rev: null, mime: null, ext: null, name: null, size: null, savedAt: null }
272
- : { active: true, pwa: c.pwa === true, rev: c.rev, mime: c.mime, ext: c.ext, name: c.name, size: c.size, savedAt: c.savedAt }
271
+ ? { active: false, pwa: false, logo: false, rev: null, mime: null, ext: null, name: null, size: null, savedAt: null }
272
+ : { active: true, pwa: c.pwa === true, logo: c.logo === true, rev: c.rev, mime: c.mime, ext: c.ext, name: c.name, size: c.size, savedAt: c.savedAt }
273
273
  }
274
274
 
275
275
  /** typert endpoint: read current status. */
@@ -282,6 +282,8 @@ export class FaviconCustomService extends TypertRemoteService {
282
282
  if (request === null || typeof request !== 'object') throw new Error('缺少图标参数')
283
283
  // `pwa`: whether this icon also replaces the PWA install icon (opt-in).
284
284
  const pwa = request.pwa === true
285
+ // `logo`: whether this icon also replaces the in-app sidebar brand mark (opt-in).
286
+ const logo = request.logo === true
285
287
  const { name, data } = request
286
288
  if (typeof data !== 'string' || data.length === 0) throw new Error('缺少图标数据')
287
289
  const { bytes } = base64Body(data)
@@ -299,7 +301,7 @@ export class FaviconCustomService extends TypertRemoteService {
299
301
  const rec = {
300
302
  id,
301
303
  mime, ext,
302
- pwa,
304
+ pwa, logo,
303
305
  name: typeof name === 'string' && name.trim().length > 0 ? name.trim().slice(0, 128) : null,
304
306
  size: bytes.length, savedAt: now, rev
305
307
  }
@@ -325,6 +327,21 @@ export class FaviconCustomService extends TypertRemoteService {
325
327
  return this.status()
326
328
  }
327
329
 
330
+ /** typert endpoint: toggle the brand-mark flag on the active icon (no re-upload). */
331
+ async setLogo(request) {
332
+ if (request === null || typeof request !== 'object' || typeof request.enabled !== 'boolean') {
333
+ throw new Error('缺少 enabled 参数')
334
+ }
335
+ const id = this.library.activeId
336
+ if (id === null || this.current === null) throw new Error('当前没有自定义图标')
337
+ const rec = this.library.icons[id]
338
+ if (rec === null || typeof rec !== 'object') throw new Error('图标记录不存在')
339
+ rec.logo = request.enabled === true
340
+ this.current.logo = rec.logo
341
+ await this._saveLibrary().catch(() => {}) // index is advisory
342
+ return this.status()
343
+ }
344
+
328
345
  /** typert endpoint: clear the active marker so the platform icon is used.
329
346
  * Icon files stay on disk for the future icon-library UI. */
330
347
  async resetIcon() {
@@ -4,6 +4,7 @@ import { z } from 'zod'
4
4
  const statusSchema = z.object({
5
5
  active: z.boolean(),
6
6
  pwa: z.boolean(),
7
+ logo: z.boolean(),
7
8
  rev: z.string().nullable(),
8
9
  mime: z.string().nullable(),
9
10
  ext: z.string().nullable(),
@@ -16,6 +17,7 @@ const setIconRequestSchema = z.object({
16
17
  mime: z.string().min(1),
17
18
  name: z.string().max(128).optional(),
18
19
  pwa: z.boolean().optional(),
20
+ logo: z.boolean().optional(),
19
21
  data: z.string().min(1)
20
22
  })
21
23
 
@@ -23,6 +25,10 @@ const setPwaRequestSchema = z.object({
23
25
  enabled: z.boolean()
24
26
  })
25
27
 
28
+ const setLogoRequestSchema = z.object({
29
+ enabled: z.boolean()
30
+ })
31
+
26
32
  const setIconResultSchema = statusSchema
27
33
 
28
34
  export const TYPERT = {
@@ -94,6 +100,31 @@ export const TYPERT = {
94
100
  },
95
101
  sourceLocation: { file: 'lib/index.js', line: 1, column: 1 }
96
102
  },
103
+ {
104
+ id: 'dsh-icon-custom#iconCustom/setLogo',
105
+ service: 'iconCustom',
106
+ namespace: 'iconCustom',
107
+ method: 'setLogo',
108
+ invocation: { kind: 'direct' },
109
+ parameters: [
110
+ {
111
+ name: 'request',
112
+ wire: 'request',
113
+ source: 'json',
114
+ codec: {
115
+ mode: 'strict',
116
+ typeSymbol: 'dsh-icon-custom#iconCustom/setLogo:request',
117
+ schema: setLogoRequestSchema
118
+ }
119
+ }
120
+ ],
121
+ result: {
122
+ mode: 'strict',
123
+ typeSymbol: 'dsh-icon-custom#iconCustom/setLogo:result',
124
+ schema: statusSchema
125
+ },
126
+ sourceLocation: { file: 'lib/index.js', line: 1, column: 1 }
127
+ },
97
128
  {
98
129
  id: 'dsh-icon-custom#iconCustom/resetIcon',
99
130
  service: 'iconCustom',
@@ -122,7 +153,7 @@ export const TYPERT = {
122
153
  {
123
154
  kind: 'method',
124
155
  name: 'getStatus',
125
- signature: 'async getStatus(): Promise<{ active: boolean; pwa: boolean; rev: string | null; mime: string | null; ext: string | null; name: string | null; size: number | null; savedAt: number | null }>',
156
+ signature: 'async getStatus(): Promise<{ active: boolean; pwa: boolean; logo: boolean; rev: string | null; mime: string | null; ext: string | null; name: string | null; size: number | null; savedAt: number | null }>',
126
157
  description: "Read the current favicon status.",
127
158
  summary: "Read the current favicon status.",
128
159
  jsDoc: "/** Read the current favicon status. */",
@@ -131,7 +162,7 @@ export const TYPERT = {
131
162
  {
132
163
  kind: 'method',
133
164
  name: 'setIcon',
134
- signature: 'async setIcon(request: { mime: string; name?: string; pwa?: boolean; data: string }): Promise<{ active: boolean; pwa: boolean; rev: string | null; mime: string | null; ext: string | null; name: string | null; size: number | null; savedAt: number | null }>',
165
+ signature: 'async setIcon(request: { mime: string; name?: string; pwa?: boolean; logo?: boolean; data: string }): Promise<{ active: boolean; pwa: boolean; logo: boolean; rev: string | null; mime: string | null; ext: string | null; name: string | null; size: number | null; savedAt: number | null }>',
135
166
  description: "Store a new favicon uploaded as a data URL.",
136
167
  summary: "Store a new favicon.",
137
168
  jsDoc: "/** Store a new favicon. */",
@@ -140,16 +171,25 @@ export const TYPERT = {
140
171
  {
141
172
  kind: 'method',
142
173
  name: 'setPwa',
143
- signature: 'async setPwa(request: { enabled: boolean }): Promise<{ active: boolean; pwa: boolean; rev: string | null; mime: string | null; ext: string | null; name: string | null; size: number | null; savedAt: number | null }>',
174
+ signature: 'async setPwa(request: { enabled: boolean }): Promise<{ active: boolean; pwa: boolean; logo: boolean; rev: string | null; mime: string | null; ext: string | null; name: string | null; size: number | null; savedAt: number | null }>',
144
175
  description: "Toggle whether the active custom icon also replaces the PWA install icon.",
145
176
  summary: "Toggle the PWA-icon option.",
146
177
  jsDoc: "/** Toggle the PWA-icon option. */",
147
178
  tags: []
148
179
  },
180
+ {
181
+ kind: 'method',
182
+ name: 'setLogo',
183
+ signature: 'async setLogo(request: { enabled: boolean }): Promise<{ active: boolean; pwa: boolean; logo: boolean; rev: string | null; mime: string | null; ext: string | null; name: string | null; size: number | null; savedAt: number | null }>',
184
+ description: "Toggle whether the active custom icon also replaces the in-app sidebar brand mark.",
185
+ summary: "Toggle the brand-mark option.",
186
+ jsDoc: "/** Toggle the brand-mark option. */",
187
+ tags: []
188
+ },
149
189
  {
150
190
  kind: 'method',
151
191
  name: 'resetIcon',
152
- signature: 'async resetIcon(): Promise<{ active: boolean; pwa: boolean; rev: string | null; mime: string | null; ext: string | null; name: string | null; size: number | null; savedAt: number | null }>',
192
+ signature: 'async resetIcon(): Promise<{ active: boolean; pwa: boolean; logo: boolean; rev: string | null; mime: string | null; ext: string | null; name: string | null; size: number | null; savedAt: number | null }>',
153
193
  description: "Clear the active custom favicon marker so the platform icon is used; stored icon files are kept.",
154
194
  summary: "Clear the custom favicon.",
155
195
  jsDoc: "/** Clear the custom favicon. */",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cowwo/dsh-icon-custom",
3
- "version": "0.2.1",
3
+ "version": "0.5.1",
4
4
  "description": "自定义浏览器标签页图标:在设置页上传 SVG/PNG/ICO,立即生效并持久保存。· Custom browser tab favicon: upload SVG/PNG/ICO from the settings page, applies instantly and persists.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",