feat: route library browsing through QML shell
This commit is contained in:
@@ -44,7 +44,7 @@
|
|||||||
| 2 | 用户首页 | 收藏 `views/user/FavoritesView` | favorites-agent | done | MediaCard/List、收藏模型 | `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/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 | 媒体浏览 | 媒体库/人物/过滤结果(共用 `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 等浏览 route、侧栏与标题导航 | `cmake --build cmake-build-debug --target qEmbyNavigationTest qEmbyApp_qmllint qEmbyApp -j2 && ./cmake-build-debug/bin/qEmbyNavigationTest`;登录→首页→搜索→返回→收藏→详情→返回→设置→退出全链路,检查窗口按钮与托盘 |
|
| 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/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 | 媒体详情 | 季 `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 | 管理 | 媒体库 `PageLibraries` | admin-libraries-agent | done | 管理卡片/Grid、拖拽排序、Library CRUD 对话框 | `cmake --build cmake-build-debug --target qEmbyApp_qmllint qEmbyApp -j2`;新增/编辑/删除/扫描/取消扫描/排序 |
|
||||||
@@ -67,7 +67,9 @@
|
|||||||
|
|
||||||
当前开发批次(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。
|
当前开发批次(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 行为保持兼容。下一批进入 Category/Library/Person/Filtered,重点解决带参数 route 的 ViewModel 实例所有权与滚动状态。
|
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 各冒烟一次。
|
阶段门禁:每阶段至少通过 QML 静态检查、Debug 构建、明暗主题、窗口缩放、键盘焦点、页面反复进入退出、网络失败/空数据检查;依赖真实服务器的行为必须用 Emby/Jellyfin 各冒烟一次。
|
||||||
|
|
||||||
|
|||||||
@@ -409,3 +409,12 @@ qemby://search?q=<percent-encoded-query>
|
|||||||
- `HomeView::triggerSearch()` 改为 push 规范 search route;同一 Router widget 内切换也会记录历史,因此 Home → Search → Back 可以恢复 Home,Search → legacy Detail → Back 可以恢复原搜索。
|
- `HomeView::triggerSearch()` 改为 push 规范 search route;同一 Router widget 内切换也会记录历史,因此 Home → Search → Back 可以恢复 Home,Search → legacy Detail → Back 可以恢复原搜索。
|
||||||
- `SearchViewModel` 增加 active/generation 取消语义;inactive 时只记录待搜索 query,重新激活后按需恢复,避免隐藏页面回写陈旧结果。
|
- `SearchViewModel` 增加 active/generation 取消语义;inactive 时只记录待搜索 query,重新激活后按需恢复,避免隐藏页面回写陈旧结果。
|
||||||
- 导航契约测试新增 search key 和返回用例;默认 Router 与 legacy fallback 的无服务器 offscreen 冒烟均无 runtime 输出。
|
- 导航契约测试新增 search key 和返回用例;默认 Router 与 legacy fallback 的无服务器 offscreen 冒烟均无 runtime 输出。
|
||||||
|
|
||||||
|
2026-07-15 完成 Phase 2 第三批 Category/Library/Person/Filtered:
|
||||||
|
|
||||||
|
- 根 Router 静态注册 CategoryPage 与 LibraryPage;Library、Person、Filtered 复用同一页面类型,但 route key 变化时重新创建 QML 页面。
|
||||||
|
- HomeView 的分类、媒体库、文件夹、人物和过滤入口统一 push 规范 route;legacy fallback 仍调用原 `create*View()`。
|
||||||
|
- Favorite Category 将 `favorite` 纳入规范 key,避免与普通 Category 使用相同身份。
|
||||||
|
- Category/Library ViewModel 的 inactive 路由更新只保存最新参数;generation 同时覆盖详情探测、首屏、渐进加载和 loadMore,防止离开后陈旧回写。
|
||||||
|
- 当前每类参数化页面仍共享一个会话 ViewModel;已加载 route 返回时复用数据,切换 route 时重建状态。每个历史 route 独立的 LRU 实例和滚动 uiState 尚未实现。
|
||||||
|
- 导航测试增加 Favorite Category 身份用例;Debug 构建、qmllint、默认 Router 与 legacy fallback 无服务器启动冒烟通过。
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ Item {
|
|||||||
property var navigationController: navigation
|
property var navigationController: navigation
|
||||||
// Keep this allow-list deliberately small until each page's context
|
// Keep this allow-list deliberately small until each page's context
|
||||||
// ViewModel has been moved to the root Quick host.
|
// ViewModel has been moved to the root Quick host.
|
||||||
property var enabledRoutes: ["home", "favorites", "search"]
|
property var enabledRoutes: ["home", "favorites", "search", "category",
|
||||||
|
"library", "person", "filtered"]
|
||||||
property var currentRoute: navigationController
|
property var currentRoute: navigationController
|
||||||
? navigationController.currentRoute
|
? navigationController.currentRoute
|
||||||
: ({ "name": "navigationError",
|
: ({ "name": "navigationError",
|
||||||
@@ -69,7 +70,9 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function routeImplemented(name) {
|
function routeImplemented(name) {
|
||||||
return name === "home" || name === "favorites" || name === "search";
|
return name === "home" || name === "favorites" || name === "search"
|
||||||
|
|| name === "category" || name === "library" || name === "person"
|
||||||
|
|| name === "filtered";
|
||||||
}
|
}
|
||||||
|
|
||||||
function routeKnown(name) {
|
function routeKnown(name) {
|
||||||
@@ -103,6 +106,11 @@ Item {
|
|||||||
case "home": return homeComponent;
|
case "home": return homeComponent;
|
||||||
case "favorites": return favoritesComponent;
|
case "favorites": return favoritesComponent;
|
||||||
case "search": return searchComponent;
|
case "search": return searchComponent;
|
||||||
|
case "category": return categoryComponent;
|
||||||
|
case "library":
|
||||||
|
case "person":
|
||||||
|
case "filtered":
|
||||||
|
return libraryComponent;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -185,6 +193,13 @@ Item {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollToTop() {
|
||||||
|
if (!pageLoader.item)
|
||||||
|
return false;
|
||||||
|
callHook(pageLoader.item, "scrollToTop", undefined);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
onCurrentRouteChanged: applyRoute(currentRoute)
|
onCurrentRouteChanged: applyRoute(currentRoute)
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
if (!pageLoader.item)
|
if (!pageLoader.item)
|
||||||
@@ -221,6 +236,8 @@ Item {
|
|||||||
Component { id: homeComponent; DashboardPage { anchors.fill: parent } }
|
Component { id: homeComponent; DashboardPage { anchors.fill: parent } }
|
||||||
Component { id: favoritesComponent; FavoritesPage { anchors.fill: parent } }
|
Component { id: favoritesComponent; FavoritesPage { anchors.fill: parent } }
|
||||||
Component { id: searchComponent; SearchPage { anchors.fill: parent } }
|
Component { id: searchComponent; SearchPage { anchors.fill: parent } }
|
||||||
|
Component { id: categoryComponent; CategoryPage { anchors.fill: parent } }
|
||||||
|
Component { id: libraryComponent; LibraryPage { anchors.fill: parent } }
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: recoveryComponent
|
id: recoveryComponent
|
||||||
|
|||||||
@@ -191,6 +191,9 @@ bool NavigationController::normalize(const QVariantMap &request,
|
|||||||
QStringList identity;
|
QStringList identity;
|
||||||
for (const QString &field : required)
|
for (const QString &field : required)
|
||||||
identity << encoded(params.value(field).toString());
|
identity << encoded(params.value(field).toString());
|
||||||
|
if (name == QLatin1String("category") &&
|
||||||
|
params.value(QStringLiteral("favorite")).toBool())
|
||||||
|
identity << QStringLiteral("favorite");
|
||||||
route->key = identity.isEmpty() ? name : name + QLatin1Char(':') + identity.join(QLatin1Char(':'));
|
route->key = identity.isEmpty() ? name : name + QLatin1Char(':') + identity.join(QLatin1Char(':'));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,14 @@ int main(int argc, char **argv)
|
|||||||
"search query participates in the route key");
|
"search query participates in the route key");
|
||||||
ok &= check(navigation.back() && navigation.currentRoute().value("name") == "home",
|
ok &= check(navigation.back() && navigation.currentRoute().value("name") == "home",
|
||||||
"back returns from search to its session root");
|
"back returns from search to its session root");
|
||||||
|
ok &= check(navigation.push({{"name", "category"},
|
||||||
|
{"params", QVariantMap{{"categoryId", "Movie"},
|
||||||
|
{"favorite", true}}}}),
|
||||||
|
"accepts a favorite category route");
|
||||||
|
ok &= check(navigation.currentRoute().value("key") ==
|
||||||
|
"category:Movie:favorite",
|
||||||
|
"favorite category has a distinct route identity");
|
||||||
|
ok &= check(navigation.back(), "returns from favorite category");
|
||||||
ok &= check(navigation.push({{"name", "library"},
|
ok &= check(navigation.push({{"name", "library"},
|
||||||
{"params", QVariantMap{{"libraryId", "lib/1"},
|
{"params", QVariantMap{{"libraryId", "lib/1"},
|
||||||
{"title", "Movies"}}}}),
|
{"title", "Movies"}}}}),
|
||||||
|
|||||||
@@ -214,119 +214,111 @@ void LibraryViewModel::setActive(bool active) {
|
|||||||
m_active = active;
|
m_active = active;
|
||||||
if (!active) {
|
if (!active) {
|
||||||
++m_generation;
|
++m_generation;
|
||||||
if (m_loading || m_loadingMore)
|
if (m_routeRequestInFlight || m_loading || m_loadingMore)
|
||||||
m_reloadOnActivate = true;
|
m_reloadOnActivate = true;
|
||||||
|
m_routeRequestInFlight = false;
|
||||||
m_loading = false;
|
m_loading = false;
|
||||||
m_loadingMore = false;
|
m_loadingMore = false;
|
||||||
emit stateChanged();
|
emit stateChanged();
|
||||||
} else if (m_reloadOnActivate) {
|
} else if (m_reloadOnActivate) {
|
||||||
m_reloadOnActivate = false;
|
m_reloadOnActivate = false;
|
||||||
launchReload();
|
launchCurrentRouteLoad();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QCoro::Task<void> LibraryViewModel::loadLibrary(QString libraryId,
|
QCoro::Task<void> LibraryViewModel::loadLibrary(QString libraryId,
|
||||||
QString libraryName) {
|
QString libraryName) {
|
||||||
|
libraryId = libraryId.trimmed();
|
||||||
|
if (isCurrentRoute(LibraryMode, libraryId, {})) {
|
||||||
|
if (m_title != libraryName) {
|
||||||
|
m_title = std::move(libraryName);
|
||||||
|
emit stateChanged();
|
||||||
|
}
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
const quint64 generation = ++m_generation;
|
const quint64 generation = ++m_generation;
|
||||||
m_mode = LibraryMode;
|
configureRoute(LibraryMode, std::move(libraryId), std::move(libraryName));
|
||||||
m_targetId = std::move(libraryId);
|
|
||||||
m_title = std::move(libraryName);
|
if (!m_active) {
|
||||||
m_filterType.clear();
|
m_reloadOnActivate = true;
|
||||||
m_filterValue.clear();
|
co_return;
|
||||||
|
}
|
||||||
|
co_await loadCurrentRoute(generation);
|
||||||
|
}
|
||||||
|
|
||||||
|
QCoro::Task<void> LibraryViewModel::loadPerson(QString personId,
|
||||||
|
QString personName) {
|
||||||
|
personId = personId.trimmed();
|
||||||
|
if (isCurrentRoute(PersonMode, personId, {})) {
|
||||||
|
if (m_title != personName) {
|
||||||
|
m_title = std::move(personName);
|
||||||
|
emit stateChanged();
|
||||||
|
}
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const quint64 generation = ++m_generation;
|
||||||
|
configureRoute(PersonMode, std::move(personId), std::move(personName));
|
||||||
|
|
||||||
|
if (!m_active) {
|
||||||
|
m_reloadOnActivate = true;
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
co_await loadCurrentRoute(generation);
|
||||||
|
}
|
||||||
|
|
||||||
|
QCoro::Task<void> LibraryViewModel::loadFiltered(QString filterType,
|
||||||
|
QString filterValue) {
|
||||||
|
filterType = filterType.trimmed();
|
||||||
|
filterValue = filterValue.trimmed();
|
||||||
|
if (isCurrentRoute(FilteredMode, filterType, filterValue))
|
||||||
|
co_return;
|
||||||
|
|
||||||
|
const quint64 generation = ++m_generation;
|
||||||
|
configureRoute(FilteredMode, std::move(filterType), std::move(filterValue));
|
||||||
|
if (!m_active) {
|
||||||
|
m_reloadOnActivate = true;
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
co_await loadCurrentRoute(generation);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LibraryViewModel::isCurrentRoute(Mode mode, const QString &primary,
|
||||||
|
const QString &secondary) const {
|
||||||
|
if (!m_routeInitialized || m_mode != mode)
|
||||||
|
return false;
|
||||||
|
if (mode == FilteredMode)
|
||||||
|
return m_filterType == primary && m_filterValue == secondary;
|
||||||
|
return m_targetId == primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LibraryViewModel::configureRoute(Mode mode, QString primary,
|
||||||
|
QString secondary) {
|
||||||
|
m_routeInitialized = true;
|
||||||
|
m_mode = mode;
|
||||||
m_currentItem = {};
|
m_currentItem = {};
|
||||||
m_tabIndex = 0;
|
m_tabIndex = 0;
|
||||||
m_firstTabText = tr("All");
|
m_firstTabText = tr("All");
|
||||||
m_tabsVisible = false;
|
m_tabsVisible = false;
|
||||||
m_favoriteVisible = false;
|
m_favoriteVisible = false;
|
||||||
|
m_reloadOnActivate = false;
|
||||||
|
|
||||||
|
if (mode == FilteredMode) {
|
||||||
|
m_targetId.clear();
|
||||||
|
m_filterType = std::move(primary);
|
||||||
|
m_filterValue = std::move(secondary);
|
||||||
|
m_title = m_filterValue;
|
||||||
|
} else {
|
||||||
|
m_targetId = std::move(primary);
|
||||||
|
m_title = std::move(secondary);
|
||||||
|
m_filterType.clear();
|
||||||
|
m_filterValue.clear();
|
||||||
|
}
|
||||||
|
|
||||||
restorePreferences();
|
restorePreferences();
|
||||||
emit stateChanged();
|
emit stateChanged();
|
||||||
emit controlsChanged();
|
emit controlsChanged();
|
||||||
|
|
||||||
if (!m_active) {
|
|
||||||
m_reloadOnActivate = true;
|
|
||||||
co_return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPointer<LibraryViewModel> guard(this);
|
|
||||||
if (m_active && m_core && m_core->mediaService() && !m_targetId.isEmpty()) {
|
|
||||||
try {
|
|
||||||
const MediaItem item =
|
|
||||||
co_await m_core->mediaService()->getItemDetail(m_targetId);
|
|
||||||
if (!guard || !m_active || generation != m_generation)
|
|
||||||
co_return;
|
|
||||||
if (item.id == m_targetId)
|
|
||||||
configureCurrentItem(item);
|
|
||||||
} catch (const std::exception &error) {
|
|
||||||
if (!guard || generation != m_generation)
|
|
||||||
co_return;
|
|
||||||
qWarning() << "[LibraryViewModel] detail probe failed:" << error.what();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!guard || !m_active || generation != m_generation)
|
|
||||||
co_return;
|
|
||||||
co_await reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
QCoro::Task<void> LibraryViewModel::loadPerson(QString personId,
|
|
||||||
QString personName) {
|
|
||||||
const quint64 generation = ++m_generation;
|
|
||||||
m_mode = PersonMode;
|
|
||||||
m_targetId = std::move(personId);
|
|
||||||
m_title = std::move(personName);
|
|
||||||
m_filterType.clear();
|
|
||||||
m_filterValue.clear();
|
|
||||||
m_currentItem = {};
|
|
||||||
m_tabIndex = 0;
|
|
||||||
m_tabsVisible = false;
|
|
||||||
m_favoriteVisible = false;
|
|
||||||
restorePreferences();
|
|
||||||
emit stateChanged();
|
|
||||||
emit controlsChanged();
|
|
||||||
|
|
||||||
if (!m_active) {
|
|
||||||
m_reloadOnActivate = true;
|
|
||||||
co_return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPointer<LibraryViewModel> guard(this);
|
|
||||||
if (m_active && m_core && m_core->mediaService() && !m_targetId.isEmpty()) {
|
|
||||||
try {
|
|
||||||
const MediaItem item =
|
|
||||||
co_await m_core->mediaService()->getItemDetail(m_targetId);
|
|
||||||
if (!guard || !m_active || generation != m_generation)
|
|
||||||
co_return;
|
|
||||||
if (item.id == m_targetId) {
|
|
||||||
m_currentItem = item;
|
|
||||||
m_favoriteVisible = true;
|
|
||||||
emit controlsChanged();
|
|
||||||
}
|
|
||||||
} catch (const std::exception &error) {
|
|
||||||
if (!guard || generation != m_generation)
|
|
||||||
co_return;
|
|
||||||
qWarning() << "[LibraryViewModel] person detail failed:" << error.what();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!guard || !m_active || generation != m_generation)
|
|
||||||
co_return;
|
|
||||||
co_await reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
QCoro::Task<void> LibraryViewModel::loadFiltered(QString filterType,
|
|
||||||
QString filterValue) {
|
|
||||||
++m_generation;
|
|
||||||
m_mode = FilteredMode;
|
|
||||||
m_filterType = std::move(filterType);
|
|
||||||
m_filterValue = std::move(filterValue);
|
|
||||||
m_targetId.clear();
|
|
||||||
m_title = m_filterValue;
|
|
||||||
m_currentItem = {};
|
|
||||||
m_tabIndex = 0;
|
|
||||||
m_tabsVisible = false;
|
|
||||||
m_favoriteVisible = false;
|
|
||||||
restorePreferences();
|
|
||||||
emit stateChanged();
|
|
||||||
emit controlsChanged();
|
|
||||||
co_await reload();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void LibraryViewModel::configureCurrentItem(const MediaItem &item) {
|
void LibraryViewModel::configureCurrentItem(const MediaItem &item) {
|
||||||
@@ -499,11 +491,72 @@ void LibraryViewModel::updateStats() {
|
|||||||
emit stateChanged();
|
emit stateChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LibraryViewModel::launchCurrentRouteLoad() {
|
||||||
|
if (!m_active || !m_routeInitialized) {
|
||||||
|
m_reloadOnActivate = m_routeInitialized;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const quint64 generation = ++m_generation;
|
||||||
|
QCoro::connect(loadCurrentRoute(generation), this, []() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
QCoro::Task<void> LibraryViewModel::loadCurrentRoute(quint64 generation) {
|
||||||
|
if (!m_active || !m_routeInitialized || generation != m_generation) {
|
||||||
|
if (!m_active)
|
||||||
|
m_reloadOnActivate = true;
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_routeRequestInFlight = true;
|
||||||
|
m_loading = true;
|
||||||
|
m_loadingMore = false;
|
||||||
|
m_hasMore = false;
|
||||||
|
m_totalCount = 0;
|
||||||
|
m_errorText.clear();
|
||||||
|
m_statsText = tr("Loading...");
|
||||||
|
m_items.clear();
|
||||||
|
emit stateChanged();
|
||||||
|
|
||||||
|
QPointer<LibraryViewModel> guard(this);
|
||||||
|
if (m_mode != FilteredMode && m_core && m_core->mediaService() &&
|
||||||
|
!m_targetId.isEmpty()) {
|
||||||
|
try {
|
||||||
|
const MediaItem item =
|
||||||
|
co_await m_core->mediaService()->getItemDetail(m_targetId);
|
||||||
|
if (!guard || !m_active || generation != m_generation)
|
||||||
|
co_return;
|
||||||
|
|
||||||
|
if (item.id == m_targetId) {
|
||||||
|
if (m_mode == PersonMode) {
|
||||||
|
m_currentItem = item;
|
||||||
|
m_favoriteVisible = true;
|
||||||
|
emit controlsChanged();
|
||||||
|
} else {
|
||||||
|
configureCurrentItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const std::exception &error) {
|
||||||
|
if (!guard || !m_active || generation != m_generation)
|
||||||
|
co_return;
|
||||||
|
qWarning() << (m_mode == PersonMode
|
||||||
|
? "[LibraryViewModel] person detail failed:"
|
||||||
|
: "[LibraryViewModel] detail probe failed:")
|
||||||
|
<< error.what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!guard || !m_active || generation != m_generation)
|
||||||
|
co_return;
|
||||||
|
m_routeRequestInFlight = false;
|
||||||
|
co_await reload();
|
||||||
|
}
|
||||||
|
|
||||||
void LibraryViewModel::launchReload() {
|
void LibraryViewModel::launchReload() {
|
||||||
if (!m_active) {
|
if (!m_active) {
|
||||||
m_reloadOnActivate = true;
|
m_reloadOnActivate = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
m_routeRequestInFlight = false;
|
||||||
QCoro::connect(reload(), this, []() {});
|
QCoro::connect(reload(), this, []() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,6 +565,7 @@ QCoro::Task<void> LibraryViewModel::reload() {
|
|||||||
m_reloadOnActivate = true;
|
m_reloadOnActivate = true;
|
||||||
co_return;
|
co_return;
|
||||||
}
|
}
|
||||||
|
m_routeRequestInFlight = false;
|
||||||
const QueryState query = buildQuery();
|
const QueryState query = buildQuery();
|
||||||
const quint64 generation = ++m_generation;
|
const quint64 generation = ++m_generation;
|
||||||
m_query = query;
|
m_query = query;
|
||||||
|
|||||||
@@ -138,6 +138,9 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
QString preferenceTargetId() const;
|
QString preferenceTargetId() const;
|
||||||
|
bool isCurrentRoute(Mode mode, const QString &primary,
|
||||||
|
const QString &secondary) const;
|
||||||
|
void configureRoute(Mode mode, QString primary, QString secondary);
|
||||||
QString sortBy() const;
|
QString sortBy() const;
|
||||||
QueryState buildQuery() const;
|
QueryState buildQuery() const;
|
||||||
bool queryValid(const QueryState &query) const;
|
bool queryValid(const QueryState &query) const;
|
||||||
@@ -146,6 +149,8 @@ private:
|
|||||||
void saveSortPreference();
|
void saveSortPreference();
|
||||||
void saveViewPreference();
|
void saveViewPreference();
|
||||||
void updateStats();
|
void updateStats();
|
||||||
|
void launchCurrentRouteLoad();
|
||||||
|
QCoro::Task<void> loadCurrentRoute(quint64 generation);
|
||||||
void launchReload();
|
void launchReload();
|
||||||
QCoro::Task<void> reload();
|
QCoro::Task<void> reload();
|
||||||
QCoro::Task<void> fetchNextPage(quint64 generation, QueryState query,
|
QCoro::Task<void> fetchNextPage(quint64 generation, QueryState query,
|
||||||
@@ -172,6 +177,8 @@ private:
|
|||||||
bool m_tabsVisible = false;
|
bool m_tabsVisible = false;
|
||||||
bool m_favoriteVisible = false;
|
bool m_favoriteVisible = false;
|
||||||
bool m_active = true;
|
bool m_active = true;
|
||||||
|
bool m_routeInitialized = false;
|
||||||
|
bool m_routeRequestInFlight = false;
|
||||||
bool m_reloadOnActivate = false;
|
bool m_reloadOnActivate = false;
|
||||||
int m_tabIndex = 0;
|
int m_tabIndex = 0;
|
||||||
int m_sortIndex = 0;
|
int m_sortIndex = 0;
|
||||||
|
|||||||
@@ -237,13 +237,24 @@ void CategoryViewModel::setActive(bool active) {
|
|||||||
m_loadingMore = false;
|
m_loadingMore = false;
|
||||||
emit stateChanged();
|
emit stateChanged();
|
||||||
} else if (m_reloadOnActivate) {
|
} else if (m_reloadOnActivate) {
|
||||||
|
const bool clearRecommendCache = m_clearRecommendCacheOnActivate;
|
||||||
m_reloadOnActivate = false;
|
m_reloadOnActivate = false;
|
||||||
launchReload();
|
m_clearRecommendCacheOnActivate = false;
|
||||||
|
launchReload(clearRecommendCache);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QCoro::Task<void> CategoryViewModel::loadCategory(QString categoryType,
|
QCoro::Task<void> CategoryViewModel::loadCategory(QString categoryType,
|
||||||
QString title) {
|
QString title) {
|
||||||
|
const bool routeChanged =
|
||||||
|
m_category != categoryType || m_title != title;
|
||||||
|
if (!routeChanged) {
|
||||||
|
if (!m_active)
|
||||||
|
m_reloadOnActivate = true;
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
++m_generation;
|
||||||
m_category = std::move(categoryType);
|
m_category = std::move(categoryType);
|
||||||
m_title = std::move(title);
|
m_title = std::move(title);
|
||||||
configureCategory();
|
configureCategory();
|
||||||
@@ -251,6 +262,15 @@ QCoro::Task<void> CategoryViewModel::loadCategory(QString categoryType,
|
|||||||
emit stateChanged();
|
emit stateChanged();
|
||||||
emit sortChanged();
|
emit sortChanged();
|
||||||
emit viewChanged();
|
emit viewChanged();
|
||||||
|
|
||||||
|
if (!m_active) {
|
||||||
|
m_loading = false;
|
||||||
|
m_loadingMore = false;
|
||||||
|
m_reloadOnActivate = true;
|
||||||
|
m_clearRecommendCacheOnActivate = false;
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
co_await reload();
|
co_await reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,6 +366,8 @@ QString CategoryViewModel::sortBy() const {
|
|||||||
void CategoryViewModel::launchReload(bool clearRecommendCache) {
|
void CategoryViewModel::launchReload(bool clearRecommendCache) {
|
||||||
if (!m_active) {
|
if (!m_active) {
|
||||||
m_reloadOnActivate = true;
|
m_reloadOnActivate = true;
|
||||||
|
m_clearRecommendCacheOnActivate =
|
||||||
|
m_clearRecommendCacheOnActivate || clearRecommendCache;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
QCoro::connect(reload(clearRecommendCache), this, []() {});
|
QCoro::connect(reload(clearRecommendCache), this, []() {});
|
||||||
@@ -354,6 +376,8 @@ void CategoryViewModel::launchReload(bool clearRecommendCache) {
|
|||||||
QCoro::Task<void> CategoryViewModel::reload(bool clearRecommendCache) {
|
QCoro::Task<void> CategoryViewModel::reload(bool clearRecommendCache) {
|
||||||
if (!m_active) {
|
if (!m_active) {
|
||||||
m_reloadOnActivate = true;
|
m_reloadOnActivate = true;
|
||||||
|
m_clearRecommendCacheOnActivate =
|
||||||
|
m_clearRecommendCacheOnActivate || clearRecommendCache;
|
||||||
co_return;
|
co_return;
|
||||||
}
|
}
|
||||||
if (!m_core || !m_core->mediaService() || m_category.isEmpty())
|
if (!m_core || !m_core->mediaService() || m_category.isEmpty())
|
||||||
@@ -361,6 +385,7 @@ QCoro::Task<void> CategoryViewModel::reload(bool clearRecommendCache) {
|
|||||||
|
|
||||||
const quint64 generation = ++m_generation;
|
const quint64 generation = ++m_generation;
|
||||||
m_reloadOnActivate = false;
|
m_reloadOnActivate = false;
|
||||||
|
m_clearRecommendCacheOnActivate = false;
|
||||||
m_loading = true;
|
m_loading = true;
|
||||||
m_loadingMore = false;
|
m_loadingMore = false;
|
||||||
m_errorText.clear();
|
m_errorText.clear();
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ private:
|
|||||||
bool m_descending = true;
|
bool m_descending = true;
|
||||||
bool m_active = true;
|
bool m_active = true;
|
||||||
bool m_reloadOnActivate = false;
|
bool m_reloadOnActivate = false;
|
||||||
|
bool m_clearRecommendCacheOnActivate = false;
|
||||||
int m_sortIndex = 0;
|
int m_sortIndex = 0;
|
||||||
quint64 m_generation = 0;
|
quint64 m_generation = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "dashboardviewmodel.h"
|
#include "dashboardviewmodel.h"
|
||||||
#include "favoritesviewmodel.h"
|
#include "favoritesviewmodel.h"
|
||||||
|
#include "categoryviewmodel.h"
|
||||||
|
|
||||||
#include "../../components/libraryactionmenu.h"
|
#include "../../components/libraryactionmenu.h"
|
||||||
#include "../../components/mediaimageeditdialog.h"
|
#include "../../components/mediaimageeditdialog.h"
|
||||||
@@ -9,6 +10,7 @@
|
|||||||
#include "../../quick/navigationcontroller.h"
|
#include "../../quick/navigationcontroller.h"
|
||||||
#include "../../quick/quickrouterhost.h"
|
#include "../../quick/quickrouterhost.h"
|
||||||
#include "../search/searchviewmodel.h"
|
#include "../search/searchviewmodel.h"
|
||||||
|
#include "../media/libraryviewmodel.h"
|
||||||
|
|
||||||
#include <qembycore.h>
|
#include <qembycore.h>
|
||||||
#include <services/admin/adminservice.h>
|
#include <services/admin/adminservice.h>
|
||||||
@@ -18,6 +20,8 @@
|
|||||||
#include <QCursor>
|
#include <QCursor>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
|
#include <QQuickItem>
|
||||||
|
#include <QQuickWidget>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
@@ -39,6 +43,8 @@ HomeQuickRouterView::HomeQuickRouterView(QEmbyCore *core,
|
|||||||
m_dashboardViewModel = new UserDashboardViewModel(core, this);
|
m_dashboardViewModel = new UserDashboardViewModel(core, this);
|
||||||
m_favoritesViewModel = new FavoritesViewModel(core, this);
|
m_favoritesViewModel = new FavoritesViewModel(core, this);
|
||||||
m_searchViewModel = new SearchViewModel(core, this);
|
m_searchViewModel = new SearchViewModel(core, this);
|
||||||
|
m_categoryViewModel = new CategoryViewModel(core, this);
|
||||||
|
m_libraryViewModel = new LibraryViewModel(core, this);
|
||||||
|
|
||||||
m_host->setContextProperty(QStringLiteral("dashboardPage"),
|
m_host->setContextProperty(QStringLiteral("dashboardPage"),
|
||||||
m_dashboardViewModel);
|
m_dashboardViewModel);
|
||||||
@@ -46,6 +52,10 @@ HomeQuickRouterView::HomeQuickRouterView(QEmbyCore *core,
|
|||||||
m_favoritesViewModel);
|
m_favoritesViewModel);
|
||||||
m_host->setContextProperty(QStringLiteral("searchPage"),
|
m_host->setContextProperty(QStringLiteral("searchPage"),
|
||||||
m_searchViewModel);
|
m_searchViewModel);
|
||||||
|
m_host->setContextProperty(QStringLiteral("categoryPage"),
|
||||||
|
m_categoryViewModel);
|
||||||
|
m_host->setContextProperty(QStringLiteral("libraryPage"),
|
||||||
|
m_libraryViewModel);
|
||||||
m_host->loadRouter();
|
m_host->loadRouter();
|
||||||
layout->addWidget(m_host);
|
layout->addWidget(m_host);
|
||||||
|
|
||||||
@@ -112,6 +122,32 @@ HomeQuickRouterView::HomeQuickRouterView(QEmbyCore *core,
|
|||||||
{
|
{
|
||||||
handleMoreMenuRequested(item, m_host->mapToGlobal(localPos));
|
handleMoreMenuRequested(item, m_host->mapToGlobal(localPos));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
connect(m_categoryViewModel, &CategoryViewModel::itemActivated,
|
||||||
|
this, &HomeQuickRouterView::forwardItemActivation);
|
||||||
|
connect(m_categoryViewModel, &CategoryViewModel::playRequested,
|
||||||
|
this, &BaseView::handlePlayRequested);
|
||||||
|
connect(m_categoryViewModel, &CategoryViewModel::favoriteRequested,
|
||||||
|
this, &BaseView::handleFavoriteRequested);
|
||||||
|
connect(m_categoryViewModel, &CategoryViewModel::moreRequested,
|
||||||
|
this, [this](const MediaItem &item)
|
||||||
|
{
|
||||||
|
handleMoreMenuRequested(item, QCursor::pos());
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_libraryViewModel, &LibraryViewModel::itemActivated,
|
||||||
|
this, &HomeQuickRouterView::forwardItemActivation);
|
||||||
|
connect(m_libraryViewModel, &LibraryViewModel::playRequested,
|
||||||
|
this, &BaseView::handlePlayRequested);
|
||||||
|
connect(m_libraryViewModel, &LibraryViewModel::favoriteRequested,
|
||||||
|
this, &BaseView::handleFavoriteRequested);
|
||||||
|
connect(m_libraryViewModel, &LibraryViewModel::currentFavoriteRequested,
|
||||||
|
this, &BaseView::handleFavoriteRequested);
|
||||||
|
connect(m_libraryViewModel, &LibraryViewModel::moreRequested,
|
||||||
|
this, [this](const MediaItem &item)
|
||||||
|
{
|
||||||
|
handleMoreMenuRequested(item, QCursor::pos());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void HomeQuickRouterView::refreshHome()
|
void HomeQuickRouterView::refreshHome()
|
||||||
@@ -135,6 +171,9 @@ void HomeQuickRouterView::scrollToTop()
|
|||||||
m_dashboardViewModel->scrollToTop();
|
m_dashboardViewModel->scrollToTop();
|
||||||
else if (routeName == QLatin1String("favorites"))
|
else if (routeName == QLatin1String("favorites"))
|
||||||
m_favoritesViewModel->scrollToTop();
|
m_favoritesViewModel->scrollToTop();
|
||||||
|
else if (m_host && m_host->quickWidget()->rootObject())
|
||||||
|
QMetaObject::invokeMethod(m_host->quickWidget()->rootObject(),
|
||||||
|
"scrollToTop");
|
||||||
}
|
}
|
||||||
|
|
||||||
void HomeQuickRouterView::onMediaItemUpdated(const MediaItem &item)
|
void HomeQuickRouterView::onMediaItemUpdated(const MediaItem &item)
|
||||||
@@ -142,12 +181,16 @@ void HomeQuickRouterView::onMediaItemUpdated(const MediaItem &item)
|
|||||||
m_dashboardViewModel->updateItem(item);
|
m_dashboardViewModel->updateItem(item);
|
||||||
m_favoritesViewModel->updateItem(item);
|
m_favoritesViewModel->updateItem(item);
|
||||||
m_searchViewModel->updateItem(item);
|
m_searchViewModel->updateItem(item);
|
||||||
|
m_categoryViewModel->updateItem(item);
|
||||||
|
m_libraryViewModel->updateItem(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
void HomeQuickRouterView::onMediaItemRemoved(const QString &itemId)
|
void HomeQuickRouterView::onMediaItemRemoved(const QString &itemId)
|
||||||
{
|
{
|
||||||
m_dashboardViewModel->removeItem(itemId);
|
m_dashboardViewModel->removeItem(itemId);
|
||||||
m_favoritesViewModel->removeItem(itemId);
|
m_favoritesViewModel->removeItem(itemId);
|
||||||
|
m_categoryViewModel->removeItem(itemId);
|
||||||
|
m_libraryViewModel->removeItem(itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
CardContextMenuRequest HomeQuickRouterView::showCardContextMenu(
|
CardContextMenuRequest HomeQuickRouterView::showCardContextMenu(
|
||||||
@@ -227,8 +270,16 @@ void HomeQuickRouterView::updateRouteActivity()
|
|||||||
m_favoritesViewModel->setActive(hostActive &&
|
m_favoritesViewModel->setActive(hostActive &&
|
||||||
routeName == QLatin1String("favorites"));
|
routeName == QLatin1String("favorites"));
|
||||||
|
|
||||||
const bool searchActive = hostActive &&
|
updateSearchRoute(route, hostActive);
|
||||||
routeName == QLatin1String("search");
|
updateCategoryRoute(route, hostActive);
|
||||||
|
updateLibraryRoute(route, hostActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeQuickRouterView::updateSearchRoute(const QVariantMap &route,
|
||||||
|
bool hostActive)
|
||||||
|
{
|
||||||
|
const QString routeName = route.value(QStringLiteral("name")).toString();
|
||||||
|
const bool searchActive = hostActive && routeName == QLatin1String("search");
|
||||||
if (!searchActive)
|
if (!searchActive)
|
||||||
{
|
{
|
||||||
m_searchViewModel->setActive(false);
|
m_searchViewModel->setActive(false);
|
||||||
@@ -255,11 +306,90 @@ void HomeQuickRouterView::updateRouteActivity()
|
|||||||
m_searchViewModel->setActive(true);
|
m_searchViewModel->setActive(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void HomeQuickRouterView::updateCategoryRoute(const QVariantMap &route,
|
||||||
|
bool hostActive)
|
||||||
|
{
|
||||||
|
const QString routeName = route.value(QStringLiteral("name")).toString();
|
||||||
|
const bool categoryActive = hostActive &&
|
||||||
|
routeName == QLatin1String("category");
|
||||||
|
if (!categoryActive)
|
||||||
|
{
|
||||||
|
m_categoryViewModel->setActive(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariantMap params = route.value(QStringLiteral("params")).toMap();
|
||||||
|
QString categoryId = params.value(QStringLiteral("categoryId"))
|
||||||
|
.toString().trimmed();
|
||||||
|
if (params.value(QStringLiteral("favorite")).toBool())
|
||||||
|
categoryId.prepend(QStringLiteral("Favorite_"));
|
||||||
|
const QString title = params.value(QStringLiteral("title")).toString();
|
||||||
|
const QString routeKey = route.value(QStringLiteral("key")).toString();
|
||||||
|
const QString routeIdentity = routeKey + QLatin1Char('\n') + title;
|
||||||
|
if (!categoryId.isEmpty() && routeIdentity != m_categoryRouteKey)
|
||||||
|
{
|
||||||
|
m_categoryViewModel->setActive(false);
|
||||||
|
m_categoryRouteKey = routeIdentity;
|
||||||
|
QCoro::connect(m_categoryViewModel->loadCategory(categoryId, title),
|
||||||
|
this, []() {});
|
||||||
|
}
|
||||||
|
m_categoryViewModel->setActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeQuickRouterView::updateLibraryRoute(const QVariantMap &route,
|
||||||
|
bool hostActive)
|
||||||
|
{
|
||||||
|
const QString routeName = route.value(QStringLiteral("name")).toString();
|
||||||
|
const bool libraryActive = hostActive &&
|
||||||
|
(routeName == QLatin1String("library") ||
|
||||||
|
routeName == QLatin1String("person") ||
|
||||||
|
routeName == QLatin1String("filtered"));
|
||||||
|
if (!libraryActive)
|
||||||
|
{
|
||||||
|
m_libraryViewModel->setActive(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariantMap params = route.value(QStringLiteral("params")).toMap();
|
||||||
|
const QString routeKey = route.value(QStringLiteral("key")).toString();
|
||||||
|
const QString title = params.value(QStringLiteral("title")).toString();
|
||||||
|
const QString routeIdentity = routeKey + QLatin1Char('\n') + title;
|
||||||
|
if (routeIdentity != m_libraryRouteKey)
|
||||||
|
{
|
||||||
|
m_libraryViewModel->setActive(false);
|
||||||
|
m_libraryRouteKey = routeIdentity;
|
||||||
|
if (routeName == QLatin1String("library"))
|
||||||
|
{
|
||||||
|
QCoro::connect(m_libraryViewModel->loadLibrary(
|
||||||
|
params.value(QStringLiteral("libraryId")).toString(),
|
||||||
|
title),
|
||||||
|
this, []() {});
|
||||||
|
}
|
||||||
|
else if (routeName == QLatin1String("person"))
|
||||||
|
{
|
||||||
|
QCoro::connect(m_libraryViewModel->loadPerson(
|
||||||
|
params.value(QStringLiteral("personId")).toString(),
|
||||||
|
title),
|
||||||
|
this, []() {});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
QCoro::connect(m_libraryViewModel->loadFiltered(
|
||||||
|
params.value(QStringLiteral("filterType")).toString(),
|
||||||
|
params.value(QStringLiteral("filterValue")).toString()),
|
||||||
|
this, []() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_libraryViewModel->setActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
void HomeQuickRouterView::deactivateRoutes()
|
void HomeQuickRouterView::deactivateRoutes()
|
||||||
{
|
{
|
||||||
m_dashboardViewModel->setActive(false);
|
m_dashboardViewModel->setActive(false);
|
||||||
m_favoritesViewModel->setActive(false);
|
m_favoritesViewModel->setActive(false);
|
||||||
m_searchViewModel->setActive(false);
|
m_searchViewModel->setActive(false);
|
||||||
|
m_categoryViewModel->setActive(false);
|
||||||
|
m_libraryViewModel->setActive(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void HomeQuickRouterView::forwardItemActivation(const MediaItem &item)
|
void HomeQuickRouterView::forwardItemActivation(const MediaItem &item)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include "../baseview.h"
|
#include "../baseview.h"
|
||||||
|
|
||||||
class FavoritesViewModel;
|
class FavoritesViewModel;
|
||||||
|
class CategoryViewModel;
|
||||||
|
class LibraryViewModel;
|
||||||
class NavigationController;
|
class NavigationController;
|
||||||
class QuickRouterHost;
|
class QuickRouterHost;
|
||||||
class SearchViewModel;
|
class SearchViewModel;
|
||||||
@@ -40,6 +42,9 @@ protected:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void updateRouteActivity();
|
void updateRouteActivity();
|
||||||
|
void updateSearchRoute(const QVariantMap &route, bool hostActive);
|
||||||
|
void updateCategoryRoute(const QVariantMap &route, bool hostActive);
|
||||||
|
void updateLibraryRoute(const QVariantMap &route, bool hostActive);
|
||||||
void deactivateRoutes();
|
void deactivateRoutes();
|
||||||
void forwardItemActivation(const MediaItem &item);
|
void forwardItemActivation(const MediaItem &item);
|
||||||
void launchDashboardTask(QCoro::Task<void> &&task);
|
void launchDashboardTask(QCoro::Task<void> &&task);
|
||||||
@@ -54,6 +59,10 @@ private:
|
|||||||
UserDashboardViewModel *m_dashboardViewModel = nullptr;
|
UserDashboardViewModel *m_dashboardViewModel = nullptr;
|
||||||
FavoritesViewModel *m_favoritesViewModel = nullptr;
|
FavoritesViewModel *m_favoritesViewModel = nullptr;
|
||||||
SearchViewModel *m_searchViewModel = nullptr;
|
SearchViewModel *m_searchViewModel = nullptr;
|
||||||
|
CategoryViewModel *m_categoryViewModel = nullptr;
|
||||||
|
LibraryViewModel *m_libraryViewModel = nullptr;
|
||||||
QString m_searchRouteKey;
|
QString m_searchRouteKey;
|
||||||
QString m_searchQuery;
|
QString m_searchQuery;
|
||||||
|
QString m_categoryRouteKey;
|
||||||
|
QString m_libraryRouteKey;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -259,9 +259,9 @@ void HomeView::setupUi()
|
|||||||
auto navigateToDetailSlot = [this](const QString &itemId, const QString &itemName, const MediaItem &seedItem)
|
auto navigateToDetailSlot = [this](const QString &itemId, const QString &itemName, const MediaItem &seedItem)
|
||||||
{ pushView(createDetailView(itemId, itemName, seedItem)); };
|
{ pushView(createDetailView(itemId, itemName, seedItem)); };
|
||||||
auto navigateToFolderSlot = [this](const QString &id, const QString &name)
|
auto navigateToFolderSlot = [this](const QString &id, const QString &name)
|
||||||
{ pushView(createLibraryView(id, name)); };
|
{ openLibraryRoute(id, name); };
|
||||||
auto navigateToPersonSlot = [this](const QString &id, const QString &name)
|
auto navigateToPersonSlot = [this](const QString &id, const QString &name)
|
||||||
{ pushView(createPersonView(id, name)); };
|
{ openPersonRoute(id, name); };
|
||||||
auto navigateToPlayerSlot = [this](const QString &id, const QString &title, const QString &url, long long ticks,
|
auto navigateToPlayerSlot = [this](const QString &id, const QString &title, const QString &url, long long ticks,
|
||||||
const QVariant &extraData) { launchPlayer(id, title, url, ticks, extraData); };
|
const QVariant &extraData) { launchPlayer(id, title, url, ticks, extraData); };
|
||||||
auto navigateToSeasonSlot = [this](const QString &seriesId, const QString &seasonId, const QString &seasonName)
|
auto navigateToSeasonSlot = [this](const QString &seriesId, const QString &seasonId, const QString &seasonName)
|
||||||
@@ -278,12 +278,12 @@ void HomeView::setupUi()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pushView(createLibraryView(id, name));
|
openLibraryRoute(id, name);
|
||||||
};
|
};
|
||||||
const auto openCategory = [this](const QString &categoryId, const QString &title)
|
const auto openCategory = [this](const QString &categoryId, const QString &title)
|
||||||
{
|
{
|
||||||
m_libraryList->clearSelection();
|
m_libraryList->clearSelection();
|
||||||
pushView(createCategoryView(categoryId, title));
|
openCategoryRoute(categoryId, title);
|
||||||
};
|
};
|
||||||
|
|
||||||
m_useQuickHomeRouter = qEnvironmentVariableIntValue("QEMBY_USE_LEGACY_HOME_PAGES") != 1;
|
m_useQuickHomeRouter = qEnvironmentVariableIntValue("QEMBY_USE_LEGACY_HOME_PAGES") != 1;
|
||||||
@@ -410,11 +410,11 @@ QWidget *HomeView::createDetailView(const QString &itemId, const QString &itemNa
|
|||||||
connect(view, &DetailView::navigateToDetail, this,
|
connect(view, &DetailView::navigateToDetail, this,
|
||||||
[this](const QString &id, const QString &name, const MediaItem &seed) { pushView(createDetailView(id, name, seed)); });
|
[this](const QString &id, const QString &name, const MediaItem &seed) { pushView(createDetailView(id, name, seed)); });
|
||||||
connect(view, &DetailView::navigateToFolder, this,
|
connect(view, &DetailView::navigateToFolder, this,
|
||||||
[this](const QString &id, const QString &name) { pushView(createLibraryView(id, name)); });
|
[this](const QString &id, const QString &name) { openLibraryRoute(id, name); });
|
||||||
connect(view, &BaseView::navigateToPerson, this,
|
connect(view, &BaseView::navigateToPerson, this,
|
||||||
[this](const QString &id, const QString &name)
|
[this](const QString &id, const QString &name)
|
||||||
{
|
{
|
||||||
pushView(createPersonView(id, name));
|
openPersonRoute(id, name);
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(view, &BaseView::navigateToPlayer, this,
|
connect(view, &BaseView::navigateToPlayer, this,
|
||||||
@@ -429,7 +429,7 @@ QWidget *HomeView::createDetailView(const QString &itemId, const QString &itemNa
|
|||||||
connect(view, &DetailView::triggerSearch, this, &HomeView::triggerSearch);
|
connect(view, &DetailView::triggerSearch, this, &HomeView::triggerSearch);
|
||||||
connect(view, &BaseView::navigateToFilteredView, this,
|
connect(view, &BaseView::navigateToFilteredView, this,
|
||||||
[this](const QString &type, const QString &value)
|
[this](const QString &type, const QString &value)
|
||||||
{ pushView(createFilteredView(type, value)); });
|
{ openFilteredRoute(type, value); });
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
@@ -449,10 +449,10 @@ QWidget *HomeView::createCategoryView(const QString &categoryId, const QString &
|
|||||||
[this](const QString &id, const QString &name, const MediaItem &seed) { pushView(createDetailView(id, name, seed)); });
|
[this](const QString &id, const QString &name, const MediaItem &seed) { pushView(createDetailView(id, name, seed)); });
|
||||||
|
|
||||||
connect(view, &CategoryView::navigateToFolder, this,
|
connect(view, &CategoryView::navigateToFolder, this,
|
||||||
[this](const QString &id, const QString &name) { pushView(createLibraryView(id, name)); });
|
[this](const QString &id, const QString &name) { openLibraryRoute(id, name); });
|
||||||
|
|
||||||
connect(view, &CategoryView::navigateToPerson, this,
|
connect(view, &CategoryView::navigateToPerson, this,
|
||||||
[this](const QString &id, const QString &name) { pushView(createPersonView(id, name)); });
|
[this](const QString &id, const QString &name) { openPersonRoute(id, name); });
|
||||||
|
|
||||||
|
|
||||||
connect(view, &BaseView::navigateToPlayer, this,
|
connect(view, &BaseView::navigateToPlayer, this,
|
||||||
@@ -481,10 +481,10 @@ QWidget *HomeView::createLibraryView(const QString &libraryId, const QString &ti
|
|||||||
[this](const QString &id, const QString &name, const MediaItem &seed) { pushView(createDetailView(id, name, seed)); });
|
[this](const QString &id, const QString &name, const MediaItem &seed) { pushView(createDetailView(id, name, seed)); });
|
||||||
|
|
||||||
connect(view, &LibraryView::navigateToFolder, this,
|
connect(view, &LibraryView::navigateToFolder, this,
|
||||||
[this](const QString &id, const QString &name) { pushView(createLibraryView(id, name)); });
|
[this](const QString &id, const QString &name) { openLibraryRoute(id, name); });
|
||||||
|
|
||||||
connect(view, &BaseView::navigateToPerson, this,
|
connect(view, &BaseView::navigateToPerson, this,
|
||||||
[this](const QString &id, const QString &name) { pushView(createPersonView(id, name)); });
|
[this](const QString &id, const QString &name) { openPersonRoute(id, name); });
|
||||||
|
|
||||||
|
|
||||||
connect(view, &BaseView::navigateToPlayer, this,
|
connect(view, &BaseView::navigateToPlayer, this,
|
||||||
@@ -520,7 +520,7 @@ QWidget *HomeView::createPersonView(const QString &personId, const QString &pers
|
|||||||
|
|
||||||
|
|
||||||
connect(view, &BaseView::navigateToPerson, this,
|
connect(view, &BaseView::navigateToPerson, this,
|
||||||
[this](const QString &id, const QString &name) { pushView(createPersonView(id, name)); });
|
[this](const QString &id, const QString &name) { openPersonRoute(id, name); });
|
||||||
|
|
||||||
|
|
||||||
connect(view, &BaseView::navigateToPlayer, this,
|
connect(view, &BaseView::navigateToPlayer, this,
|
||||||
@@ -556,10 +556,10 @@ QWidget *HomeView::createSearchView(const QString &query)
|
|||||||
[this](const QString &id, const QString &name, const MediaItem &seed) { pushView(createDetailView(id, name, seed)); });
|
[this](const QString &id, const QString &name, const MediaItem &seed) { pushView(createDetailView(id, name, seed)); });
|
||||||
|
|
||||||
connect(view, &SearchView::navigateToFolder, this,
|
connect(view, &SearchView::navigateToFolder, this,
|
||||||
[this](const QString &id, const QString &name) { pushView(createLibraryView(id, name)); });
|
[this](const QString &id, const QString &name) { openLibraryRoute(id, name); });
|
||||||
|
|
||||||
connect(view, &SearchView::navigateToPerson, this,
|
connect(view, &SearchView::navigateToPerson, this,
|
||||||
[this](const QString &id, const QString &name) { pushView(createPersonView(id, name)); });
|
[this](const QString &id, const QString &name) { openPersonRoute(id, name); });
|
||||||
|
|
||||||
|
|
||||||
connect(view, &BaseView::navigateToPlayer, this,
|
connect(view, &BaseView::navigateToPlayer, this,
|
||||||
@@ -589,9 +589,9 @@ QWidget *HomeView::createFilteredView(const QString &filterType, const QString &
|
|||||||
connect(view, &BaseView::navigateToDetail, this,
|
connect(view, &BaseView::navigateToDetail, this,
|
||||||
[this](const QString &id, const QString &name, const MediaItem &seed) { pushView(createDetailView(id, name, seed)); });
|
[this](const QString &id, const QString &name, const MediaItem &seed) { pushView(createDetailView(id, name, seed)); });
|
||||||
connect(view, &BaseView::navigateToFolder, this,
|
connect(view, &BaseView::navigateToFolder, this,
|
||||||
[this](const QString &id, const QString &name) { pushView(createLibraryView(id, name)); });
|
[this](const QString &id, const QString &name) { openLibraryRoute(id, name); });
|
||||||
connect(view, &BaseView::navigateToPerson, this,
|
connect(view, &BaseView::navigateToPerson, this,
|
||||||
[this](const QString &id, const QString &name) { pushView(createPersonView(id, name)); });
|
[this](const QString &id, const QString &name) { openPersonRoute(id, name); });
|
||||||
connect(view, &BaseView::navigateToPlayer, this,
|
connect(view, &BaseView::navigateToPlayer, this,
|
||||||
[this](const QString &id, const QString &title, const QString &url, long long ticks,
|
[this](const QString &id, const QString &title, const QString &url, long long ticks,
|
||||||
const QVariant &extraData) { launchPlayer(id, title, url, ticks, extraData); });
|
const QVariant &extraData) { launchPlayer(id, title, url, ticks, extraData); });
|
||||||
@@ -998,7 +998,7 @@ void HomeView::setupSidebar()
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pushView(createLibraryView(viewId, cleanName));
|
openLibraryRoute(viewId, cleanName);
|
||||||
});
|
});
|
||||||
|
|
||||||
m_sidebarAnim = new QPropertyAnimation(m_sidebar, "pos", this);
|
m_sidebarAnim = new QPropertyAnimation(m_sidebar, "pos", this);
|
||||||
@@ -1301,6 +1301,59 @@ bool HomeView::pushQuickRoute(const QVariantMap &request)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void HomeView::openCategoryRoute(const QString &categoryId, const QString &title)
|
||||||
|
{
|
||||||
|
const bool favorite = categoryId.startsWith(QLatin1String("Favorite_"));
|
||||||
|
const QString normalizedId = favorite ? categoryId.mid(9) : categoryId;
|
||||||
|
const QVariantMap request{
|
||||||
|
{QStringLiteral("name"), QStringLiteral("category")},
|
||||||
|
{QStringLiteral("title"), title},
|
||||||
|
{QStringLiteral("params"),
|
||||||
|
QVariantMap{{QStringLiteral("categoryId"), normalizedId},
|
||||||
|
{QStringLiteral("title"), title},
|
||||||
|
{QStringLiteral("favorite"), favorite}}}};
|
||||||
|
if (!pushQuickRoute(request))
|
||||||
|
pushView(createCategoryView(categoryId, title));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeView::openLibraryRoute(const QString &libraryId, const QString &title)
|
||||||
|
{
|
||||||
|
const QVariantMap request{
|
||||||
|
{QStringLiteral("name"), QStringLiteral("library")},
|
||||||
|
{QStringLiteral("title"), title},
|
||||||
|
{QStringLiteral("params"),
|
||||||
|
QVariantMap{{QStringLiteral("libraryId"), libraryId},
|
||||||
|
{QStringLiteral("title"), title}}}};
|
||||||
|
if (!pushQuickRoute(request))
|
||||||
|
pushView(createLibraryView(libraryId, title));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeView::openPersonRoute(const QString &personId, const QString &title)
|
||||||
|
{
|
||||||
|
const QVariantMap request{
|
||||||
|
{QStringLiteral("name"), QStringLiteral("person")},
|
||||||
|
{QStringLiteral("title"), title},
|
||||||
|
{QStringLiteral("params"),
|
||||||
|
QVariantMap{{QStringLiteral("personId"), personId},
|
||||||
|
{QStringLiteral("title"), title}}}};
|
||||||
|
if (!pushQuickRoute(request))
|
||||||
|
pushView(createPersonView(personId, title));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeView::openFilteredRoute(const QString &filterType,
|
||||||
|
const QString &filterValue)
|
||||||
|
{
|
||||||
|
const QVariantMap request{
|
||||||
|
{QStringLiteral("name"), QStringLiteral("filtered")},
|
||||||
|
{QStringLiteral("title"), filterValue},
|
||||||
|
{QStringLiteral("params"),
|
||||||
|
QVariantMap{{QStringLiteral("filterType"), filterType},
|
||||||
|
{QStringLiteral("filterValue"), filterValue},
|
||||||
|
{QStringLiteral("title"), filterValue}}}};
|
||||||
|
if (!pushQuickRoute(request))
|
||||||
|
pushView(createFilteredView(filterType, filterValue));
|
||||||
|
}
|
||||||
|
|
||||||
void HomeView::navigateBack()
|
void HomeView::navigateBack()
|
||||||
{
|
{
|
||||||
QWidget *current = m_contentSwitcher->currentWidget();
|
QWidget *current = m_contentSwitcher->currentWidget();
|
||||||
|
|||||||
@@ -117,6 +117,10 @@ private:
|
|||||||
|
|
||||||
void pushView(QWidget* view);
|
void pushView(QWidget* view);
|
||||||
bool pushQuickRoute(const QVariantMap &request);
|
bool pushQuickRoute(const QVariantMap &request);
|
||||||
|
void openCategoryRoute(const QString &categoryId, const QString &title);
|
||||||
|
void openLibraryRoute(const QString &libraryId, const QString &title);
|
||||||
|
void openPersonRoute(const QString &personId, const QString &title);
|
||||||
|
void openFilteredRoute(const QString &filterType, const QString &filterValue);
|
||||||
void resetToView(QWidget* view, const QString &rootRoute = QString());
|
void resetToView(QWidget* view, const QString &rootRoute = QString());
|
||||||
QWidget* homeRootWidget() const;
|
QWidget* homeRootWidget() const;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user