我一直在使用 Playnite 管理游戏库。在桌面主题中,Dune 很符合我的审美:宽松的排版、圆角卡片、干净的层次,以及很明显的 WinUI 3 / Fluent Design 气质。遗憾的是,它是一个较新的主题,扩展兼容性远不如一些成熟主题;部分插件页面会出现黑底黑字,另一些插件则完全没有位置显示。
于是,我和 Codex 用一天时间把它逐步改造成了 Dune Rev:一个由我维护、保留原作视觉语言,同时补充扩展兼容性与自定义能力的个人 fork。

起点:好看的主题,不完整的生态
Dune 原本声明支持 SuccessStory、HowLongToBeat、Extra Metadata Loader 和 ScreenshotsVisualizer,基础方向已经很清楚,但我日常使用的插件远不止这些。作为参照,Mythic 对 BackgroundChanger、DuplicateHider、GameActivity、Library Management、ThemeExtras、Steam 新闻与评论、ThemeModifier 等扩展都有成熟集成。
最初看到的问题只是“设置页面颜色不对”,真正开始检查 XAML 后才发现,这并不是简单补几种颜色就能结束的工作:Playnite 主题不仅负责外观,还承担了许多插件控件的宿主、资源字典、可见性条件和布局契约。一个缺失的资源键可能让文字不可读;一个名称不正确的ContentControl 会让插件无法注入内容;而两个视图复用同一份播放状态,甚至会产生只有在来回切换视图后才出现的故障。
把插件数据变成主题的一部分
这次修改并没有简单地把所有插件面板堆到页面底部。我更希望扩展数据看起来像 Dune 原生界面的一部分,因此重新组织了详情页的概览区域:最后游玩时间、累计游玩时长、完成状态、成就、活动记录、HowLongToBeat 和 SystemChecker 都使用同一套卡片语言。
这些卡片遵循几个规则:
- 插件没有安装,或当前游戏没有对应数据时,卡片自动隐藏;
- 剩余卡片自动换行,不为缺失项目留下空洞;
- 可以打开扩展详情的卡片带有 Hover 动画,提示它不是静态信息;
- HowLongToBeat 只在卡片中显示 Main Story,避免 Main、Main+ 和 100% 挤在一起;
- 完整图表、进度条或列表仍保留在下方的独立标签页中。
Playnite Achievements 是一个特殊情况。它本身带有主题迁移机制,因此显式添加一套“原生支持”反而会与迁移后的控件重复或冲突。最终方案是让插件继续负责迁移,主题只提供它需要的颜色和控件资源。这个过程也提醒我:插件兼容并不总是“写得越多越好”,首先要理解插件已经承担了什么工作。
目前 Dune Rev 覆盖的主要扩展包括:
| 功能 | 扩展 |
|---|---|
| 成就 | SuccessStory、Playnite Achievements 自动迁移 |
| 替代背景 | BackgroundChanger |
| 重复副本 | DuplicateHider |
| 功能图标 | Library Management |
| 收藏与完成状态 | ThemeExtras |
| 活动记录 | GameActivity |
| 通关时间估算 | HowLongToBeat |
| Logo 与视频 | Extra Metadata Loader |
| 截图 | ScreenshotsVisualizer |
| Steam 新闻与在线人数 | Steam News and Players Viewer |
| Steam 评论 | Review Viewer |
| 系统需求 | SystemChecker |
| 主题调整 | ThemeModifier |
最难处理的并不是“显示”,而是状态
Extra Metadata Loader 的 Logo 与视频集成带来了几个很典型的问题。早期版本在详情页没有显示Logo,而是退回到了游戏图标;修正控件宿主后,视频又出现了更隐蔽的状态错误:从详情视图切换到 Grid-Details View,再切回来,英雄图区域就无法重新播放视频,而 Grid 视图始终正常。
原因是两个视图同时使用了缓存的标准播放器、相同的 RadioButton 组名,并绑定到插件的全局 IsVideoPlaying 状态。视图虽然切换了,播放状态和宿主却没有真正隔离。最终我为两个视图使用独立配置的播放器实例、不同的媒体选择组,并让播放按钮绑定到各自播放器的状态。这个 bug 很适合作为 WPF/XAML 的一个小例子:界面消失不代表状态也消失,名称作用域之外还可能存在由插件缓存的对象。
Details View 与 Grid-Details View 不是同一种布局
另一个持续反复的问题是宽度。详情视图适合宽屏内容:左侧有概览卡片与扩展标签页,右侧是开发者、发行商、评分和功能特色。Grid-Details View 则通常只是网格右侧的一条窄栏。如果把两者当成同一套横向布局,结果不是信息栏吞掉所有空间,就是卡片被压缩成细长的小块。
因此,两种视图最终拥有了独立的尺寸和排列规则:
- Details View 默认最大内容宽度为 1760 px,信息栏最大宽度为 480 px;
- Grid View 默认详情栏宽度为 640 px;
- Grid 中的概览卡片可以换行,信息面板移动到卡片下方;
- Logo、卡片、扩展面板、按钮和顶部栏尺寸都可独立调整。

这些设置被暴露给 ThemeModifier。它的主题配置格式目前只有一条 Description 字符串,没有按语言提供名称的机制,所以我采用了 English / 简体中文 的双语标签,并统一按“全局布局、详情视图、网格视图、扩展面板、网格封面”等区域分组。最终共有 26 个经过核对、确实被主题使用的调节项,而不只是清单里看得见、实际不起作用的滑条。
为什么选择 Fluent Dim
Dune 原本的深色模式就注明可能存在问题。真正麻烦的是,Playnite 10 没有为插件注入的控件提供可靠、统一的深浅色状态:主题自己的控件可以切换,插件迁移或注入的页面却未必跟随。结果往往是浅色背景上的浅色文字,或深色背景上的黑色文字。
与其维护两套始终有一部分失效的配色,我最终选择只保留一套 Fluent Dim:比传统浅色模式更适合夜间,又不像纯黑主题那样产生强烈对比。它使用深灰蓝表面、较柔和的白色文字和 Fluent风格的半透明层次。这个决定牺牲了主题切换,但换来了主界面、设置窗口和插件内容之间稳定得多的一致性。
从修改版到可以发布的 fork
随着修改越来越多,它已经不适合继续被当成一份本地补丁。我将项目命名为 Dune Rev,生成了新的 GUID,更新主题名称、作者与仓库链接,并补齐 README、CHANGELOG、.gitignore、许可证和 Playnite 的 Add-on / Installer manifests。
原作采用 MIT License,因此 fork 和再发布是允许的,但必须保留原始版权与许可声明。Dune Rev 的许可证中保留了 sakasakiking 的原始版权信息,并加入我的修改声明。
发布前还进行了几层验证:
- 对全部 XAML 文件做 XML 解析;
- 使用 Playnite 的 32 位程序集按真实资源加载顺序测试字典;
- 执行 Release 构建并确认没有编译错误;
- 用 Playnite Toolbox 生成
.pthm安装包并检查包内文件; - 核对 ThemeModifier 配置、插件清单、截图与下载 URL。
最终的 1.0.0 安装包已经准备完成。接下来只需要建立 GitHub Release,并把 Add-on manifest 提交到官方的 PlayniteAddonDatabase,就可以申请进入 Playnite 内置的附加组件浏览器。
总结
这次工作最初只是想修复几个插件页面的颜色,最后却变成了对主题布局、插件数据、状态管理和发布流程的完整梳理。它也说明了 Playnite 主题开发很有意思的一点:XAML 不只是“皮肤”,而是核心程序与多个独立扩展之间的一层用户界面协议。
Dune Rev 仍然保留了我最喜欢的 Dune 外观,但现在它更像一个可以长期使用的完整主题。更重要的是,这个项目已经从我电脑上的一组修改,变成了有独立身份、可验证、可更新,也可以公开发布的开源项目。
本篇博客由我与 GPT-5.6 Sol High 撰写
