16 KiB
qEmby QML 迁移状态
更新时间:2026-07-15
结论与口径
当前架构是 Widgets 应用壳 + QQuickWidget 局部页面,还不是纯 QML 应用:MainWindow、登录/主页导航、侧栏、标题栏和页面栈仍由 Widgets 负责;QuickPageHost 把 QML 页面嵌入 QStackedWidget/SlidingStackedWidget。迁移完成的判定必须同时满足“QML 页面存在、由实际导航入口加载、核心交互可用”,不能只以 .qml 文件存在为准。
代码依据:
src/qEmbyApp/CMakeLists.txt的qt_add_qml_module(qEmbyApp ...)是当前 QML 模块清单。src/qEmbyApp/quick/quickpagehost.cpp使用QQuickWidget,并处理主题底色、context property、image provider 与加载错误。src/qEmbyApp/views/settings/settingsview.cpp实际加载 5 个设置 QML 页面。src/qEmbyApp/views/admin/manageview.cpp保留 Widgets 管理壳,但仪表盘和六个管理页的页面主体均已由各自的QuickPageHost加载 QML。src/qEmbyApp/views/user/homeview.cpp仍负责主路由,并创建 Dashboard/Favorites/Category/Library/Detail/Season/Search/Player/Settings/Manage 等 QWidget 页面。src/qEmbyApp/quick/navigationcontroller.*已实现规范 route、参数/权限校验、会话清理和 UI state;HomeView已旁路接入并与旧m_navStack并行运行。
状态口径:done = 已接入实际入口;in-progress = 已分配并正在迁移;ready = 前置基本具备,可领取;blocked = 应先完成列出的依赖;legacy = 迁移期间刻意保留的原生边界。
已迁移并接入
| 子系统 | 页面 | QML / 数据层 | owner | status | 验收命令 |
|---|---|---|---|---|---|
| 设置 | 通用 | SettingsGeneralPage.qml / SettingsViewModel |
core | done | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2 |
| 设置 | 外观 | SettingsAppearancePage.qml / SettingsViewModel |
core | done | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2 |
| 设置 | 媒体库 | SettingsLibraryPage.qml / SettingsViewModel |
core | done | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2 |
| 设置 | 播放器 | SettingsPlayerPage.qml / SettingsViewModel |
core | done | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2 |
| 设置 | 关于 | SettingsAboutPage.qml / AboutViewModel |
core | done | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2 |
| 管理 | 仪表盘 | ManageDashboardPage.qml / DashboardViewModel、EmbyImageProvider |
core | done | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2;登录管理员账号后检查 2 秒刷新、重启/关机确认框 |
说明:设置与管理的左侧导航容器仍是 Widgets;About 的许可证窗口以及管理仪表盘的确认框/Toast 仍通过 Widgets 打开,因此这里的 done 指页面主体,不代表其父壳和所有弹窗都已迁移。
待迁移清单
以下 owner 是任务归属,不是 C++ 类所有者。login-agent 与 search-agent 已按本轮协作安排标为进行中。
| 阶段 | 子系统 | 页面/范围(现有 Widgets 事实) | owner | status | 依赖 | 验收命令 |
|---|---|---|---|---|---|---|
| 1 | 公共入口 | 登录 views/public/LoginView |
login-agent | done | 主题输入框、按钮、忙碌/错误状态;认证 ViewModel | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2;冷启动后完成服务器选择、登录成功/失败各一次 |
| 1 | 搜索 | 搜索结果 views/search/SearchView |
search-agent | done | 媒体列表模型、MediaCard、分页/空态 | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2;提交关键词并验证结果、空结果、进入详情、返回 |
| 1 | 公共基础 | QML 弹窗/Toast/确认框、菜单、空态/错误态 | feedback-agent | done | Theme、现有 ModernDialog.qml |
cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2;键盘 Esc/Enter、焦点回收、明暗主题各冒烟一次 |
| 1 | 公共基础 | MediaCard + 可复用 List/Grid + 图片加载/占位/错误 | media-primitives-agent | done | EmbyImageProvider 泛化、QAbstractListModel 角色契约 |
cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2;100+ 项滚动、图片失败、键鼠激活检查 |
| 2 | 用户首页 | 首页 views/user/DashboardView |
dashboard-agent | done | MediaCard/List、首页 section 模型、请求取消 | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2;继续观看/最新/推荐/媒体库分区加载与跳转 |
| 2 | 用户首页 | 收藏 views/user/FavoritesView |
favorites-agent | done | MediaCard/List、收藏模型 | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2;分类、详情、文件夹、人物跳转及返回 |
| 2 | 用户首页 | 分类 views/user/CategoryView |
category-agent | done | MediaCard/List、筛选/分页模型 | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2;分类切换、分页、空态、返回状态保持 |
| 2 | 媒体浏览 | 媒体库/人物/过滤结果(共用 views/media/LibraryView) |
library-agent | done | MediaCard/Grid、排序筛选菜单、分页模型 | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2;海报/平铺视图、排序筛选、滚动续载、人物页 |
| 2 | 应用壳 | MainWindow 标题栏、全局搜索、HomeView 侧栏与路由栈 |
core | in-progress | NavigationController、单引擎 Router.qml、Home/Favorites/Search/Category/Library/Person/Filtered 已接入;待 Detail/Season、侧栏与标题导航 |
cmake --build cmake-build-debug --target qEmbyNavigationTest qEmbyApp_qmllint qEmbyApp -j2 && ./cmake-build-debug/bin/qEmbyNavigationTest;登录→首页→分类→媒体库→人物/过滤→搜索→返回→详情→返回→退出全链路,检查窗口按钮与托盘 |
| 3 | 媒体详情 | 详情 views/media/DetailView |
detail-agent | done | MediaCard、Flow/Tag、动作菜单、编辑/识别弹窗桥接 | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2;电影/剧集/人物各一项,播放、收藏、版本选择、编辑入口 |
| 3 | 媒体详情 | 季 views/media/SeasonView |
season-agent | done | Episode 列表模型、MediaCard、播放动作 | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2;切季、剧集续播/播放、已播状态、返回 |
| 3 | 管理 | 媒体库 PageLibraries |
admin-libraries-agent | done | 管理卡片/Grid、拖拽排序、Library CRUD 对话框 | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2;新增/编辑/删除/扫描/取消扫描/排序 |
| 3 | 管理 | 合集/播放列表 PageCollections |
admin-collections-agent | done | 管理卡片/Grid、拖拽排序、文本/确认弹窗 | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2;创建/重命名/删除/批量删除/排序 |
| 3 | 管理 | 转码 PageTranscoding |
admin-transcoding-agent | done | 表单组件、折叠区、编解码器模型与复杂校验 | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2;加载、修改、保存、校验失败、编解码器参数弹窗 |
| 3 | 管理 | 用户 PageUsers |
admin-users-agent | done | 用户卡片模型、Add/Edit User 对话框 | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2;新增、编辑、启停、删除及权限错误 |
| 3 | 管理 | 任务 PageTasks |
admin-tasks-agent | done | 任务列表模型、TaskEdit 对话框 | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2;启动、停止、编辑任务及轮询刷新 |
| 3 | 管理 | 日志 PageLogs |
admin-logs-agent | done | 日志树/列表、文件保存桥接 | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2;日志列表、内容加载、失败态、另存为 |
| 4 | 播放 | 播放页面/OSD views/media/PlayerView 与 components/player* |
unassigned | blocked | QML 视频承载方案、PlaybackManager 契约、所有 OSD/字幕/弹幕组件 | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2;libmpv 播放/暂停/seek/音量/字幕/弹幕/全屏/退出,重复 10 次无崩溃 |
| 4 | 对话框 | 媒体编辑/识别/图片、播放列表、下载、WebDAV、代理、许可证等 components/*dialog* |
unassigned | blocked | QML Overlay/Dialog 框架;文件选择器与原生能力桥接 | cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2;按对话框建立交互矩阵,覆盖接受/取消/错误/窗口关闭 |
| 4 | 平台集成 | 系统托盘、无边框窗口、文件选择器、外部播放器/IPC | platform | legacy | 纯 QML 壳稳定后再决定是否包装;不要求强行 QML 重写 | cmake --build cmake-build-debug --target qEmbyApp -j2;Linux/Windows/macOS 各执行关闭到托盘、恢复、退出和窗口按钮冒烟 |
建议阶段与依赖顺序
- 固定 QML 契约:先完成登录、搜索,以及弹窗/Toast、MediaCard/List/Grid;统一 ViewModel 的 loading/error/empty/cancelled 生命周期。没有这些基础件,后续页面会复制临时控件和异步处理。
- 迁移高复用浏览链路:Dashboard、Favorites、Category、Library/Person/Filtered。它们共享卡片、图片、分页和导航,是验证模型契约与性能的最佳批次。
- 替换应用壳:浏览链路稳定后,再把 MainWindow/HomeView 的标题栏、侧栏、路由栈换成 QML;期间
QuickPageHost继续充当兼容层。壳迁移必须保留托盘、窗口按钮、主题切换和返回栈行为。 - 迁移复杂业务页:详情/季与六个剩余管理页。先提取 C++ ViewModel/
QAbstractListModel,QML 不直接持有 service 协程或业务缓存。 - 最后处理播放与长尾弹窗:播放器涉及 libmpv/OpenGL/原生窗口和输入焦点,是最高风险项。平台集成可以保留 C++,目标应是“QML 表现层 + C++ 原生能力”,而不是消灭所有 QWidget/C++。
当前开发批次(2026-07-15):导航 Phase 0/1 已落地,Phase 2 首批完成。新增强类型 NavigationRoute、NavigationController 和轻量契约测试;Widgets 页面统一先转换为规范 route,再进入旧页面栈。QuickRouterHost 以单个 QML engine 承载 Router.qml,Home/Favorites 共享该 host 与图片 provider,并由 HomeQuickRouterView 协调 ViewModel 生命周期和 BaseView 媒体动作。其他 route 暂时保持旧 Widgets host,切入时 Router 保留底层页面且暂停生命周期,避免过渡闪屏。可用 QEMBY_USE_LEGACY_HOME_PAGES=1 临时回退旧 Home/Favorites 双 host。
Phase 2 第二批(2026-07-15):Search 已加入根 Router。HomeView::triggerSearch() 现在生成规范 search route;同一 Quick host 内的 Home/Favorites/Search 切换只更新 Controller/Loader,进入 legacy 详情页时仍保存兼容历史。SearchViewModel 新增 active/generation 生命周期,隐藏或离开搜索时淘汰在途结果,返回时仅按需恢复;旧 SearchView 默认 active 行为保持兼容。
Phase 2 第三批(2026-07-15):Category、Library、Person、Filtered 已加入根 Router。HomeView 的首页媒体库、侧栏媒体库、文件夹、人物和过滤入口统一生成规范 route;Favorite Category 在 key 中具有独立身份。Category/Library ViewModel 在 inactive 时只保存最新参数,并用 generation 淘汰详情探测、首屏与 loadMore 的过期回写;旧包装器继续保持默认 active 兼容。当前 Category 和 Library 系列各使用一个会话级 ViewModel,返回已完成 route 时可复用数据,切换参数时重建当前状态;尚未实现每个历史 route 独立的 LRU ViewModel 与滚动位置。下一批优先接 Detail/Season,再统一 uiState.scrollY。
阶段门禁:每阶段至少通过 QML 静态检查、Debug 构建、明暗主题、窗口缩放、键盘焦点、页面反复进入退出、网络失败/空数据检查;依赖真实服务器的行为必须用 Emby/Jellyfin 各冒烟一次。
公共组件与基础设施缺口
- 导航:声明式 route、参数、返回栈、页面缓存/销毁策略;当前契约散落在
HomeView::create*View()与动态routeType属性中。 - 异步状态:统一
loading/error/empty/refreshing、请求去重、页面销毁后的取消/弱引用;当前 Dashboard VM 是可参考样例,但尚无通用基类。 - 媒体模型:统一
QAbstractListModelroles、分页、排序、筛选、收藏/已播状态增量更新,避免 QML 读取复杂 Core DTO。 - 视觉组件:MediaCard、Section、IconButton、Menu/ContextMenu、Dialog/MessageBox、Toast、Busy/Skeleton、Error/Empty、Tabs、Tag/Chip、拖拽排序、文件/目录选择入口。
- 图片:现有
EmbyImageProvider/AdaptiveIconProvider只在管理仪表盘 host 中显式注册;全局 QML 引擎需要统一注册、缓存策略、取消和失败占位。 - 可访问性与输入:焦点环、Tab 顺序、快捷键、右键菜单、触屏/高 DPI、屏幕阅读器语义。
- 测试:仓库当前没有 qEmbyApp 页面级自动测试;应增加 Qt Quick Test(组件/VM)与最小导航冒烟,避免只靠截图判断。
- 国际化:新 QML 文案需进入现有
qEmby_zh_CN.ts/qEmby_fr_FR.ts流程,并核验运行时语言切换/重启语义。
高风险边界
- QQuickWidget 只是过渡层:它使用离屏 FBO;仓库已经为透明合成黑底加了不透明 clear color。嵌入页越多,渲染、焦点、弹窗层级和显存成本越高,应避免把每个小组件都拆成独立
QQuickWidget。 - 播放器:
PlayerView、MpvWidget、OpenGL 与大量player*控件耦合,且有全屏、长按、字幕、弹幕、媒体源切换。先做单一播放器原型和压力冒烟,再决定QQuickFramebufferObject/原生纹理/保留 QWidget 视频面方案。 - 生命周期与协程:页面快速返回、切服务器或退出时,QCoro 任务、WebSocket 与定时刷新不能回调已销毁的 QML 对象。管理仪表盘已有 active/hidden 控制,但尚未形成全局规则。
- 业务逻辑回流 QML:现有 Widgets 页面体量大(尤其详情、转码、媒体库管理);迁移应先抽取 ViewModel/模型,不能把 API 编排、配置序列化和缓存策略复制进 JavaScript。
- 导航状态兼容:
HomeView当前重建历史页面并依赖 routeId/routeTitle/routeExtraId;切换为 QML 路由时必须保留人物、过滤、季、管理、设置等返回参数和滚动状态。 - 原生能力:托盘、无边框窗口、文件保存、外部播放器 IPC 适合继续放在 C++;“全面迁移到 QML”应限定为 UI/交互层迁移。
- 主题双栈:Widgets 仍走 QSS/
ThemeManager,QML 走QuickTheme/Themed controls。过渡期必须以同一 token 为源,验证实时明暗切换,避免两套颜色和尺寸继续分叉。
每个迁移任务的完成定义
- 实际导航已切到 QML,旧 Widgets 页面不再是默认路径;需要回退时必须有明确 feature flag。
- C++ 只暴露稳定属性、模型、信号和 invokable;QML 不直接编排网络 service 或持有 Core DTO。
qEmbyApp_qmllint与qEmbyApp构建通过,无新增 QML runtime error/warning。- loading/error/empty/disabled、键鼠焦点、明暗主题、窗口缩放和中英文至少完成一次验证。
- 页面进入/退出及刷新至少重复 10 次;涉及异步或播放器的任务额外验证退出、切服务器和应用关闭。
- 删除旧实现应单独提交,在 QML 路径稳定后进行,便于回退与差异核对。