# 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 各执行关闭到托盘、恢复、退出和窗口按钮冒烟 | ## 建议阶段与依赖顺序 1. **固定 QML 契约**:先完成登录、搜索,以及弹窗/Toast、MediaCard/List/Grid;统一 ViewModel 的 loading/error/empty/cancelled 生命周期。没有这些基础件,后续页面会复制临时控件和异步处理。 2. **迁移高复用浏览链路**:Dashboard、Favorites、Category、Library/Person/Filtered。它们共享卡片、图片、分页和导航,是验证模型契约与性能的最佳批次。 3. **替换应用壳**:浏览链路稳定后,再把 MainWindow/HomeView 的标题栏、侧栏、路由栈换成 QML;期间 `QuickPageHost` 继续充当兼容层。壳迁移必须保留托盘、窗口按钮、主题切换和返回栈行为。 4. **迁移复杂业务页**:详情/季与六个剩余管理页。先提取 C++ ViewModel/`QAbstractListModel`,QML 不直接持有 service 协程或业务缓存。 5. **最后处理播放与长尾弹窗**:播放器涉及 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 是可参考样例,但尚无通用基类。 - **媒体模型**:统一 `QAbstractListModel` roles、分页、排序、筛选、收藏/已播状态增量更新,避免 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` 流程,并核验运行时语言切换/重启语义。 ## 高风险边界 1. **QQuickWidget 只是过渡层**:它使用离屏 FBO;仓库已经为透明合成黑底加了不透明 clear color。嵌入页越多,渲染、焦点、弹窗层级和显存成本越高,应避免把每个小组件都拆成独立 `QQuickWidget`。 2. **播放器**:`PlayerView`、`MpvWidget`、OpenGL 与大量 `player*` 控件耦合,且有全屏、长按、字幕、弹幕、媒体源切换。先做单一播放器原型和压力冒烟,再决定 `QQuickFramebufferObject`/原生纹理/保留 QWidget 视频面方案。 3. **生命周期与协程**:页面快速返回、切服务器或退出时,QCoro 任务、WebSocket 与定时刷新不能回调已销毁的 QML 对象。管理仪表盘已有 active/hidden 控制,但尚未形成全局规则。 4. **业务逻辑回流 QML**:现有 Widgets 页面体量大(尤其详情、转码、媒体库管理);迁移应先抽取 ViewModel/模型,不能把 API 编排、配置序列化和缓存策略复制进 JavaScript。 5. **导航状态兼容**:`HomeView` 当前重建历史页面并依赖 routeId/routeTitle/routeExtraId;切换为 QML 路由时必须保留人物、过滤、季、管理、设置等返回参数和滚动状态。 6. **原生能力**:托盘、无边框窗口、文件保存、外部播放器 IPC 适合继续放在 C++;“全面迁移到 QML”应限定为 UI/交互层迁移。 7. **主题双栈**: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 路径稳定后进行,便于回退与差异核对。