Skip to main content
G

dsh-client-ui-wallpaper

gnk478/dsh-client-ui-wallpaper

把本地图片/视频(Dynamic Wallpaper.app 素材)用作 DSH 桌面客户端壁纸:铺满窗口、背景模糊、面板/左右侧栏独立透明度、自动深浅字、视频缩略图、轮换与播放列表同步。· Use local images or videos as the DSH desktop client background.

Install

dsh plugin --profile web add github:gnk478/dsh-client-ui-wallpaper

README

dsh-client-ui-wallpaper

check npm

把本地图片 / 视频(例如 Dynamic Wallpaper.app 播放列表里的素材) 用作 DSH 桌面客户端的背景:整窗铺满、背景模糊、面板与左右侧栏透明度分别可控,字色跟着壁纸明暗自动切换。

这是 ~/.dsh/profiles/desktop/plugins/dsh-client-ui-wallpaper 的完整项目版:含挂载声明、安装脚本、自检脚本与文档;已发布到 npm(dsh-client-ui-wallpaper)。

设置 · 壁纸

上图为真实运行截图(macOS 客户端,设置 → 壁纸):轮换选择与「23 项参与轮换」、自动轮换间隔 60 分钟、只轮换深色 5 项 / 浅色 17 项、浅色壁纸用深色字、自动加入新壁纸、毛玻璃 9px、右侧栏透明度 70%,下方是动态壁纸缩略图网格(每格右上角标注该壁纸的明暗判定)。截图为整幅实拍原图(未裁切、未遮挡、未修饰),仅等比缩放到 1800px 宽。

功能

背景

  • 图片或视频铺满整窗(object-fit: cover),面板半透明让壁纸透出来;弹窗保持不透明
  • 背景模糊可调(0–60px);可选纱层(浅色外观白纱 / 深色外观黑纱)
  • 静态与动态分开:图片可配「同名视频」,网格里用 ▶ 动态 一键切换
  • 轮换:间隔 15 秒–24 小时可调;支持「全选 / 仅深色动态 / 清空 / 默认」

可读性(墨水)

  • 按壁纸亮度自动切深/浅字(含代码块、行内 code、气泡、工具栏、输入框与 caret)
  • 代码块只跟外观走:浅色外观浅底深字、深色外观深底浅字(绕开 pre.shiki 透明背景导致的误判)
  • 设置弹窗内的字色复位为「该外观本来的值」,不被壁纸影响

面板与控件(设置 → 壁纸)

控件说明
壁纸网格静态/动态分组,缩略图 + 深/浅角标 + ✓/+ 轮换勾选 + 当前/播放中标记
背景模糊滑块 + − [数字框] + 精确步进(0–60,居中、无原生箭头)
右侧栏透明度0–100% 线性,独立于面板;左栏保持原透明观感
只轮换深色 / 只轮换浅色优先用缩略图实测亮度判定,未采样时按配置名单取反
自动加入新壁纸新丢进目录的素材自动入库并追加进轮换
自动轮换 / 间隔秒数可调;可勾「随机轮换」(不会连续两次同一张)
手动选择不打断轮换点缩略图(静态或动态)只换「当前这一张」,轮换照旧(1.1.6 起);选中的素材即使不在轮换列表里也先显示它,下一拍回到列表继续
视频省电暂停窗口不可见时暂停播放,回到前台自动续播(隐藏期间不切新壁纸)
轮换过渡交叉淡入淡出 0.7s;系统开启「减少动态效果」或窗口隐藏时直接切换
首帧就绪再淡入新壁纸等首帧解码完成(视频 loadeddata、图片 load/decode,最多等 600ms)才开始淡入,避免大视频「先透明后硬切」
图层自愈每 10 秒审计一次壁纸层:除面纱与当前节点外,活过过渡宽限(700ms + 250ms)的节点一律判定为残留并回收;层内每个节点的 tag/来源/暂停/透明度/存活时长都上报到 /_probe 的 layerDetail
探针实时化每次切换壁纸后 0.6 秒、以及每 60 秒再发一次 /_probe,switches / layerDetail / audit / prewarm 都是最新值(1.1.1 之前只在启动后 1.5 秒上报一次)
大文件不预热预热前先取素材大小(1.1.4 起宿主在 list.json 里直接给 sizes,取不到才回退 HEAD),超过 50MB 的素材不提前建节点(库里最大的视频 196MB),/_probe 的 prewarm 记录 warmed / skipped / dropped 与最近一次的字节数
加载失败不静默list.json 加载链一旦抛错,错误名与 message 会记进 loadFailure(/wallpaper/_probe 与 /wallpaper/_client 都带),并在下一次成功加载后清空——不再出现「面板正常但壁纸整块空白、没有任何线索」
选择器失效不静默客户端每 60 秒体检 8 组界面挂钩(输入框/侧栏/右栏/弹窗/气泡/工具栏/头像/代码块):带锚点(textarea、[class*="sidebar"]、[aria-modal="true"] 等)的一组连续两轮都匹配不到才算失效(避开 React 渲染中途的假警报),此时 console.warn 并把 missing / detail 写进 /wallpaper/_probe 的 selectorHealth——DSH 升级后「样式悄悄没生效」有据可查
浅色壁纸用深色字关掉就固定用浅色字
同步播放列表一键把 Dynamic Wallpaper.app 播放列表里的素材同步进来
删除(缩略图悬停)移到「废纸篓」(可恢复)+ 删缩略图缓存 + 从轮换移除 + 记入同步跳过名单
侧栏小圆钉与头像中心对齐的收起按钮,点空白处自动收回

安装

要求宿主 DSH ≥ 0.2.0-rc.2(本插件实际测试的版本)。装完刷新页面(或重启 DSH),打开 设置 → 壁纸。

A. 桌面 App:App 内插件市场(推荐)

DSH Desktop 的 desktop profile 由 Electron 应用独占管理,命令行会直接拒绝(profile "desktop" is managed exclusively by the Electron application),所以桌面端请在 设置 → 插件市场 里搜索安装。本插件已提交社区注册表 awesome-dsh-plugin(条目文件),收录后即可在市场一键安装与升级。

B. 其他 profile(web / 自建 profile):一行命令

dsh plugin --profile web add dsh-client-ui-wallpaper

CLI 会把包装进 profile 的 node_modules,并自动把包名追加到 profile 的 dsh.profile.bundles——cordis.patch.yml 不需要手动改。命令行需要 pnpm 在 PATH。

升级要写版本号:pnpm 11 默认 minimumReleaseAge=24h,发布未满 24 小时的版本,dsh plugin add <包名> 与 @latest 都只会装到上一版(并提示 x.y.z is available)。要立刻升级请指定版本:

dsh plugin --profile web add dsh-client-ui-wallpaper@1.1.8

C. 手动安装(fallback)

profile 已经被手动改过、或环境里没有 dsh 命令时用;会直接改 profile 的 cordis.patch.yml(先备份):

npm i dsh-client-ui-wallpaper
node node_modules/dsh-client-ui-wallpaper/scripts/install.mjs --profile desktop

从源码同理:

git clone https://github.com/gnk478/dsh-client-ui-wallpaper.git
cd dsh-client-ui-wallpaper
node scripts/install.mjs --profile desktop

profile 的 package.json / cordis.patch.yml 归插件管理器所有,手写的 insert 行可能在它下次操作时被覆盖——能用市场或 CLI 就不要用它。

快速开始

素材二选一:

  • 什么都不用做:默认直接读 Dynamic Wallpaper.app 的素材库 ~/Library/Containers/whbalzac.Dongtaizhuomian/Data/Documents/{Wallpaper,Videos}
  • 用自己的目录:在 config 里写绝对路径(插件不做 ~ 展开),例如 imageDir: /Users/you/.dsh/wallpapers、videoDir: /Users/you/.dsh/videos
mkdir -p ~/.dsh/wallpapers ~/.dsh/videos
cp ~/Pictures/some.jpg ~/.dsh/wallpapers/          # 静态壁纸
cp ~/Movies/some.mp4  ~/.dsh/videos/               # 动态壁纸

node scripts/check.mjs                             # 自检

配置

config(cordis.patch.yml 的 ui-wallpaper 行,见 examples/profile-cordis.patch.yml)

键默认说明
imageDir / videoDirDynamic Wallpaper.app 素材库的 Wallpaper/ 与 Videos/(lib/index.js:37-39)素材目录,必须是绝对路径(插件不做 ~ 展开)
modeimageimage / video / rotate
image / video''固定的文件名
livefalse图片有同名视频时用视频做动态壁纸
rotateSeconds300轮换间隔(秒)
shufflefalse随机轮换(不会连续两次同一张)
blur8背景模糊(px)
panelOpacity0面板不透明度(0 = 全透)
windowOpacity0.96弹窗不透明度
dim0纱层强度
autoInktrue按壁纸亮度自动切字色
autoIncludetrue新素材自动加入轮换
only / darkVideos[]深色壁纸 / 深色动态白名单

state(~/.dsh/wallpaper-state.json,面板里改的都落这里)

mode、image、video、live、rotation、only、videoPool、blur、panelOpacity、sidebarOpacity(null = 跟随面板)、autoInk、autoInclude、knownFiles、lastAdded、rotateSeconds、shuffle

# 直接读/写状态(调试用)
curl -s  http://127.0.0.1:19387/wallpaper/state
curl -s -X POST -H 'content-type: application/json' \
     -d '{"sidebarOpacity":0.35}' http://127.0.0.1:19387/wallpaper/state

与 Dynamic Wallpaper.app 同步

app 的播放列表在:

~/Library/Containers/whbalzac.Dongtaizhuomian/Data/Documents/
  ├── Videos/                播放列表素材
  ├── Wallpaper/             静态壁纸
  └── Setting/Preferences.json   playlist_array(播放列表)

两种同步方式(都只增不删;读取该目录需要给 DSH「完全磁盘访问权限」):

# 面板:设置 → 壁纸 → 同步播放列表
# 或命令行
./sync-wallpaper-playlist.sh            # 只同步播放列表里的视频
./sync-wallpaper-playlist.sh --stills   # 连静态壁纸目录一起

同步会跳过 ~/.dsh/wallpaper-sync-skip.txt 里列出的文件名(删除过的素材会自动写进去,防止被拉回)。

目录结构

dsh-client-ui-wallpaper/
├── lib/
│   ├── index.js                 宿主:路由 / 扫描 / 状态 / 抽帧 / 同步 / 删除
│   └── client.js                客户端:注入样式 / 铺背景 / 面板 UI / 墨水判定
├── scripts/
│   ├── install.mjs              安装进 profile(备份 + 复制 + 写 insert 行)
│   └── check.mjs                静态自检(语法 + 路由 + 样式常量 + 控件)
├── docs/ARCHITECTURE.md         架构、路由表、样式常量、踩坑记录
├── .github/workflows/           check.yml(自检)+ publish.yml(npm 可信发布)
├── examples/profile-cordis.patch.yml  带完整配置的挂载示例
├── examples/github-workflow-publish.yml  publish.yml 副本(方便复制)
├── cordis.patch.yml             本插件的挂载声明
├── CHANGELOG.md / PROVENANCE.md / LICENSE
└── README.md

开发

  • 宿主是普通 cordis 插件,改完由 DSH 的 HMR 重载;客户端 lib/client.js 改动会被热替换(注意:热替换会重置模块级变量,需要跨次保留的数据要放 window / sessionStorage)

  • 自检:

    node scripts/check.mjs
    node --test                                          # 行为测试(宿主:路由 / 回收站 / 缩略图 GC;客户端:轮换加载与预热 / crossfade / 首帧就绪 / 图层审计 / applyVars / autoInk / classifyTile / syncBlur / 选择器体检 / 手选不打断轮换 / 手选点击留痕),1.1.7 起共 54 例
    curl -s http://127.0.0.1:19387/wallpaper/_client | python3 -m json.tool | head -40   # 客户端自报状态
    curl -s http://127.0.0.1:19387/wallpaper/_hits                                        # 各路由请求计数
    

发布与打包

npm 发布走可信发布(Trusted Publishing / OIDC),不需要 token:

  1. 改 package.json 的 version、更新 CHANGELOG.md
  2. 在 npm 包设置里配 Trusted Publisher:GitHub Actions → Organization gnk478、Repository dsh-client-ui-wallpaper、Workflow publish.yml,Allowed actions 勾上 npm publish
  3. 推 tag 或手动触发 .github/workflows/publish.yml:
git tag v1.1.8 && git push origin v1.1.8   # tag 触发
gh workflow run publish.yml                 # 或手动触发

工作流里 permissions: id-token: write 是关键(OIDC 身份);npm publish 会自动带 provenance。 本地手工发布仍然可用,但需要一个开了 Bypass 2FA 的 Granular Access Token:npm login && npm publish(prepublishOnly 会先跑 scripts/check.mjs 与 node --test)。

Release 附件:

git archive --format=zip -o dsh-client-ui-wallpaper-1.1.8.zip HEAD
gh release create v1.1.8 dsh-client-ui-wallpaper-1.1.8.zip

CI:.github/workflows/check.yml 在 push / PR 时跑 node scripts/check.mjs 与 node --test,也是顶部徽章的来源;两个工作流的副本放在 examples/ 下方便复制。

故障排查

症状原因 / 处理
壁纸没铺满面板里确认已选壁纸;检查 /wallpaper/list.json 是否有素材
点了壁纸完全没反应(1.1.6)面板 onClick 用的 modeAfterPick() 被定义在 apply() 内,闭包取不到 → 每次点击同步抛 ReferenceError,save() 从未执行。1.1.7 已把函数提升到工厂作用域;排查时读 /wallpaper/_probe 的 panelPick(点击计数:数字不涨 = 点击没进来;涨了但画面不变 = 状态没生效)与 /_client 的 lastError
点了壁纸,自动轮换就停了(1.1.5 及更早)面板手选曾经硬编码 mode: "image" / mode: "video",等于顺手把轮换关掉;1.1.6 起改走 modeAfterPick()——轮换开着时手选只改当前位置,选中的素材不在轮换列表里也先显示它
壁纸整块空白(面板一切正常)1.1.1 / 1.1.2 的已知缺陷:rotate 分支丢了 var seconds / var index 声明,load() 一进分支就抛 ReferenceError,又被静默的 .catch 吞掉 → 壁纸再也不绘制且毫无提示。升级到 1.1.3;之后若再遇到,读 /wallpaper/_probe 的 loadFailure({ at, name, message })
DSH 升级后样式或自动字色悄悄没生效1.1.5 起客户端每 60 秒体检一次界面挂钩,读 /wallpaper/_probe 的 selectorHealth:missing 列出失效的挂钩(missing: [] 即正常),detail 给出每组的匹配数与命中的那条选择器;带锚点的组连续两轮落空才会告警,所以渲染中途不会误报。告警同时会 console.warn("[dsh-wallpaper] UI 选择器失效:…")
侧栏/右栏透明度拖了没反应常见于「透明化扫描」清掉了底色;本版本已对右栏跳过扫描并直接写 background-color。若自行改过选择器,核对 SIDEBAR_CSS / SIDEBAR_LEFT_CSS
代码块字看不清代码块应「只跟外观走」;检查 CODEFIX_CSS 是否注入
视频缩略图一直是灰的首次访问 /wallpaper/thumb/<name> 会调 qlmanage 抽帧,稍等再刷新;检查 ~/.dsh/wallpaper-thumbs/
换壁纸没有淡入效果首次绘制、重绘同一张、窗口隐藏、或系统开了「减少动态效果」(prefers-reduced-motion: reduce)时都是直接切换,不做 0.7s 过渡。1.1.1 起淡入会先等新壁纸首帧就绪(最多 600ms),大视频不会再看成「秒切」;用 /wallpaper/_probe 的 switches(lastReason / waited)可确认最近一次为什么没淡。1.1.2 起这些统计是实时的(切换后 0.6 秒 + 每 60 秒刷新),另外 layerDetail 会逐个列出层里残留的节点、audit.removed 显示自愈回收了几个。loadFailure 为 null 说明加载链没抛错
新素材不出现刷新面板(宿主目录扫描有 4 秒快照缓存,见 lib/index.js:229);确认扩展名在白名单(图片 jpg/jpeg/png/webp/gif/avif/bmp,视频 mp4/webm/mov/m4v)
改了 profile 文件不生效DSH 运行中会用内存配置回写 profile 文件;完全退出后再改,或改用「设置」界面
面板打不开 / 报错看 /wallpaper/_client 的 lastError,以及宿主日志

已知限制

  • 与 DSH 的布局类名(_6Qf49G_*)和色板 token(--dsw-*)耦合,DSH 升级后可能需要同步调整;1.1.5 起这类失效不再静默——每 60 秒体检一次,selectorHealth.missing 会点名落空的那组(detail 给出匹配数)
  • 播放列表同步依赖 macOS 的 TCC 权限(完全磁盘访问)与 qlmanage(缩略图抽帧)
  • 删除会移到 ~/.Trash(可恢复);素材与废纸篓不在同一卷时 rename 失败,会回退为真删,面板提示「已永久删除」
  • 超过 50MB 的素材不做轮换预热,切换时的首次解码在几十毫秒到几百毫秒之间(首帧就绪上限仍是 600ms)
  • 图层自愈是兜底手段:正常情况 audit.removed 恒为 0;若它持续增长,layerDetail 会指出是哪个节点、来自哪个文件
  • 轮换分支的初始化(seconds / index / 首帧停在哪一项)现在由 test/rotate.test.mjs 用真实源码 + 替身 fetch 跑通:任何未声明变量或提前抛错都会让测试变红,而不是让壁纸静默空白

License

MIT

Related plugins