Featured image of post 从 Dune 到 Dune Rev:一次 Playnite 主题兼容性改造记录

从 Dune 到 Dune Rev:一次 Playnite 主题兼容性改造记录

为 Dune 补全扩展兼容性、重新设计详情布局,并将它整理为可独立发布的 Playnite 桌面主题

我一直在使用 Playnite 管理游戏库。在桌面主题中,Dune 很符合我的审美:宽松的排版、圆角卡片、干净的层次,以及很明显的 WinUI 3 / Fluent Design 气质。遗憾的是,它是一个较新的主题,扩展兼容性远不如一些成熟主题;部分插件页面会出现黑底黑字,另一些插件则完全没有位置显示。

于是,我和 Codex 用一天时间把它逐步改造成了 Dune Rev:一个由我维护、保留原作视觉语言,同时补充扩展兼容性与自定义能力的个人 fork。

Dune Rev 详情视图
Dune Rev 的详情视图;概览卡片和扩展内容会根据可用数据自适应显示

起点:好看的主题,不完整的生态

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、卡片、扩展面板、按钮和顶部栏尺寸都可独立调整。
Dune Rev 网格详情视图
Grid-Details View 使用专门的窄栏布局,而不是压缩宽屏详情页

这些设置被暴露给 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 的原始版权信息,并加入我的修改声明。

发布前还进行了几层验证:

  1. 对全部 XAML 文件做 XML 解析;
  2. 使用 Playnite 的 32 位程序集按真实资源加载顺序测试字典;
  3. 执行 Release 构建并确认没有编译错误;
  4. 用 Playnite Toolbox 生成 .pthm 安装包并检查包内文件;
  5. 核对 ThemeModifier 配置、插件清单、截图与下载 URL。

最终的 1.0.0 安装包已经准备完成。接下来只需要建立 GitHub Release,并把 Add-on manifest 提交到官方的 PlayniteAddonDatabase,就可以申请进入 Playnite 内置的附加组件浏览器。

总结

这次工作最初只是想修复几个插件页面的颜色,最后却变成了对主题布局、插件数据、状态管理和发布流程的完整梳理。它也说明了 Playnite 主题开发很有意思的一点:XAML 不只是“皮肤”,而是核心程序与多个独立扩展之间的一层用户界面协议。

Dune Rev 仍然保留了我最喜欢的 Dune 外观,但现在它更像一个可以长期使用的完整主题。更重要的是,这个项目已经从我电脑上的一组修改,变成了有独立身份、可验证、可更新,也可以公开发布的开源项目。

本篇博客由我与 GPT-5.6 Sol High 撰写