feat: complete QML shell migration and ship 0.0.8 Arch package
Replace the Widgets main shell with AppShellHost + QML routes, add a QML independent player window, and remove the legacy player/page Widgets stack. Also wire settings cache/proxy actions, fix quit-time engine teardown, improve list/home scroll performance, and update Arch packaging to 0.0.8.
This commit is contained in:
+6
-1
@@ -1,6 +1,6 @@
|
|||||||
cmake_minimum_required(VERSION 3.19)
|
cmake_minimum_required(VERSION 3.19)
|
||||||
|
|
||||||
project(qEmby VERSION 0.0.6 LANGUAGES CXX)
|
project(qEmby VERSION 0.0.8 LANGUAGES CXX)
|
||||||
|
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
set(CMAKE_OSX_DEPLOYMENT_TARGET 13.0)
|
set(CMAKE_OSX_DEPLOYMENT_TARGET 13.0)
|
||||||
@@ -184,6 +184,11 @@ unset(_qemby_saved_skip_install_rules)
|
|||||||
set(QWINDOWKIT_INSTALL OFF CACHE BOOL
|
set(QWINDOWKIT_INSTALL OFF CACHE BOOL
|
||||||
"Disable QWindowKit development install rules inside qEmby" FORCE)
|
"Disable QWindowKit development install rules inside qEmby" FORCE)
|
||||||
|
|
||||||
|
# QML 外壳迁移需要 QWK::QuickWindowAgent 为 ApplicationWindow 提供无边框窗口;
|
||||||
|
# 上游默认只构建 widgets 模块。
|
||||||
|
set(QWINDOWKIT_BUILD_QUICK ON CACHE BOOL
|
||||||
|
"Build QWindowKit quick module for the QML shell" FORCE)
|
||||||
|
|
||||||
add_subdirectory(libs/qwindowkit)
|
add_subdirectory(libs/qwindowkit)
|
||||||
add_subdirectory(libs/qwindowkit/examples/shared)
|
add_subdirectory(libs/qwindowkit/examples/shared)
|
||||||
add_subdirectory(src/qEmbyCore)
|
add_subdirectory(src/qEmbyCore)
|
||||||
|
|||||||
+53
-84
@@ -1,104 +1,73 @@
|
|||||||
# qEmby QML 迁移状态
|
# qEmby QML 迁移状态
|
||||||
|
|
||||||
更新时间:2026-07-15
|
更新时间:2026-07-27(设置缺口补全)
|
||||||
|
|
||||||
## 结论与口径
|
## 结论
|
||||||
|
|
||||||
当前架构是 **Widgets 应用壳 + `QQuickWidget` 局部页面**,还不是纯 QML 应用:`MainWindow`、登录/主页导航、侧栏、标题栏和页面栈仍由 Widgets 负责;`QuickPageHost` 把 QML 页面嵌入 `QStackedWidget`/`SlidingStackedWidget`。迁移完成的判定必须同时满足“QML 页面存在、由实际导航入口加载、核心交互可用”,不能只以 `.qml` 文件存在为准。
|
**唯一外壳 = 全 QML**(`AppShellHost` + `AppShell.qml`)。
|
||||||
|
Widgets 页面壳与播放器栈已删除。
|
||||||
|
**独立播放窗 = 第二个 QML `ApplicationWindow`**(`IndependentPlayerHost` + `IndependentPlayerWindow.qml`)。
|
||||||
|
|
||||||
代码依据:
|
保留 `QApplication`:托盘、文件对话框、业务 Widget 对话框(弹幕设置/识别、字幕设置、库/用户管理等)仍需要。
|
||||||
|
|
||||||
- `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` = 迁移期间刻意保留的原生边界。
|
| 模式 | 实现 |
|
||||||
|
|---|---|
|
||||||
|
| 壳内嵌入 | `PlayerRouteController` → 路由 `player` → `PlayerPage` |
|
||||||
|
| 独立窗口 | `IndependentPlayerHost` → 二级引擎 → `IndependentPlayerWindow` |
|
||||||
|
| 外部播放器 | `PlaybackManager` + `ExternalPlayerIPC` |
|
||||||
|
|
||||||
## 已迁移并接入
|
## 已删除(播放器栈 + 无引用 Widgets)
|
||||||
|
|
||||||
| 子系统 | 页面 | QML / 数据层 | owner | status | 验收命令 |
|
**播放器:** `PlayerWindow` `PlayerView` `NativeDanmakuOverlay` `MpvWidget` 及 OSD/切集/统计/长按/seek bubble 等。
|
||||||
|---|---|---|---|---|---|
|
|
||||||
| 设置 | 通用 | `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` 指页面主体,不代表其父壳和所有弹窗都已迁移。
|
**孤儿 UI(约 35 组,无 live `#include` 路径):**
|
||||||
|
`MediaGridWidget` `Horizontal*Gallery` `MediaCardDelegate` `MediaListModel`
|
||||||
|
`Detail*Widget` `AdminUserCard` `CollectionCard/Grid`
|
||||||
|
`SearchHistoryPopup`(Widgets 版,QML 已有)`ServerWheelView`
|
||||||
|
`DashboardSectionOrderWidget/Strip` `DanmakuServerDialog*`
|
||||||
|
`LibraryActionMenu` `TranscodingCodecList*`
|
||||||
|
`SettingsCard/SubPanel`(Widgets 版)`ThemeTransitionWidget` `ShadowOverlay`
|
||||||
|
`ShimmerWidget` `ModernScrollPanel` `ModernSortButton` `ModernNumberInput` 等。
|
||||||
|
|
||||||
## 待迁移清单
|
## 仍保留的 Widgets
|
||||||
|
|
||||||
以下 owner 是任务归属,不是 C++ 类所有者。`login-agent` 与 `search-agent` 已按本轮协作安排标为进行中。
|
| 角色 | 代表 |
|
||||||
|
|---|---|
|
||||||
|
| 播放对话框 | `PlayerDanmaku*Dialog` `PlayerSubtitleSettingsDialog` `PlayerOverlayDialog` |
|
||||||
|
| 管理/库 | `LibraryCreate/EditDlg` `UserEditDialog` `TaskEditDialog` `*Codec*Dialog` |
|
||||||
|
| 媒体操作 | `MediaActionMenu` `MediaIdentify/ImageEditDialog` `AddToPlaylistDialog` |
|
||||||
|
| 通用 | `ModernDialogBase` `ModernToast` `ModernMessageBox` `ProxySettingsDialog` `WebDav*` |
|
||||||
|
| 播放核心 | `MpvController` `PlayerDanmakuController` |
|
||||||
|
|
||||||
| 阶段 | 子系统 | 页面/范围(现有 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 各执行关闭到托盘、恢复、退出和窗口按钮冒烟 |
|
|
||||||
|
|
||||||
## 建议阶段与依赖顺序
|
```bash
|
||||||
|
cmake --build cmake-build-debug --target qEmbyApp -j$(nproc)
|
||||||
|
./cmake-build-debug/bin/qEmbyApp
|
||||||
|
```
|
||||||
|
|
||||||
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。
|
| 功能 | 位置 |
|
||||||
|
|---|---|
|
||||||
|
| 主页区块自定义排序(↑↓ + 重置) | `SettingsLibraryPage` + `SettingsViewModel::{customHomeSectionOrderEnabled,homeSectionOrder,moveHomeSection}` |
|
||||||
|
| 弹幕 API 服务器管理(选/启停/增删改) | `SettingsPlayerPage` + `DanmakuSettings` 桥接 |
|
||||||
|
| 缺失弹幕属性 | `danmakuSourceMode` / `AutoLoad` / `AutoMatch` / `WithRelated` / `CacheHours` |
|
||||||
|
| 库开关按服务器作用域 | `showContinueWatching` 等写入 `server/<id>/library/...`(与 Dashboard 一致,兼容读全局旧值) |
|
||||||
|
|
||||||
Phase 2 第二批(2026-07-15):Search 已加入根 Router。`HomeView::triggerSearch()` 现在生成规范 `search` route;同一 Quick host 内的 Home/Favorites/Search 切换只更新 Controller/Loader,进入 legacy 详情页时仍保存兼容历史。`SearchViewModel` 新增 active/generation 生命周期,隐藏或离开搜索时淘汰在途结果,返回时仅按需恢复;旧 `SearchView` 默认 active 行为保持兼容。
|
## 本轮优化(2026-07-27)
|
||||||
|
|
||||||
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`。
|
| 项 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| 设置缓存清理 | `clearRecommendCache` / `clearImageCache` 已接线 + toast |
|
||||||
|
| 代理设置 | 设置-通用「配置…」打开 `ProxySettingsDialog` |
|
||||||
|
| 列表性能 | MediaCard DPR sourceSize、滑动关 smooth/mipmap;Grid/Section 减小 cacheBuffer |
|
||||||
|
| 首页 | 区块 Loader 异步;reduceMotion 跳过入场动画 |
|
||||||
|
| 路由 | reduceMotion / 播放器页瞬时切页,少缩放动画 |
|
||||||
|
|
||||||
阶段门禁:每阶段至少通过 QML 静态检查、Debug 构建、明暗主题、窗口缩放、键盘焦点、页面反复进入退出、网络失败/空数据检查;依赖真实服务器的行为必须用 Emby/Jellyfin 各冒烟一次。
|
## 后续可选
|
||||||
|
|
||||||
## 公共组件与基础设施缺口
|
- 播放器内弹幕设置 / 识别 / 字幕对话框 QML 化
|
||||||
|
- 主页排序改为拖拽(当前为上下按钮,足够用)
|
||||||
- **导航**:声明式 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 路径稳定后进行,便于回退与差异核对。
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ if(UNIX AND NOT APPLE)
|
|||||||
"${CMAKE_INSTALL_LIBDIR}/${QEMBY_RUNTIME_LIB_SUBDIR}")
|
"${CMAKE_INSTALL_LIBDIR}/${QEMBY_RUNTIME_LIB_SUBDIR}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
foreach(_qemby_runtime_target IN ITEMS qEmbyApp qEmbyCore QWKCore QWKWidgets)
|
foreach(_qemby_runtime_target IN ITEMS qEmbyApp qEmbyCore QWKCore QWKWidgets QWKQuick)
|
||||||
if(TARGET "${_qemby_runtime_target}")
|
if(TARGET "${_qemby_runtime_target}")
|
||||||
set_target_properties("${_qemby_runtime_target}" PROPERTIES
|
set_target_properties("${_qemby_runtime_target}" PROPERTIES
|
||||||
INSTALL_RPATH "$ORIGIN"
|
INSTALL_RPATH "$ORIGIN"
|
||||||
@@ -18,7 +18,7 @@ if(UNIX AND NOT APPLE)
|
|||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
set(_qemby_qwk_install_targets)
|
set(_qemby_qwk_install_targets)
|
||||||
foreach(_qemby_qwk_target IN ITEMS QWKCore QWKWidgets)
|
foreach(_qemby_qwk_target IN ITEMS QWKCore QWKWidgets QWKQuick)
|
||||||
if(TARGET "${_qemby_qwk_target}")
|
if(TARGET "${_qemby_qwk_target}")
|
||||||
list(APPEND _qemby_qwk_install_targets "${_qemby_qwk_target}")
|
list(APPEND _qemby_qwk_install_targets "${_qemby_qwk_target}")
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Maintainer: local
|
# Maintainer: local
|
||||||
|
|
||||||
pkgname=qemby
|
pkgname=qemby
|
||||||
pkgver=0.0.7
|
pkgver=0.0.8
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Desktop client for Emby and Jellyfin media servers"
|
pkgdesc="Desktop client for Emby and Jellyfin media servers"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
@@ -15,6 +15,7 @@ depends=(
|
|||||||
'mpv'
|
'mpv'
|
||||||
'qcoro'
|
'qcoro'
|
||||||
'qt6-base'
|
'qt6-base'
|
||||||
|
'qt6-declarative'
|
||||||
'qt6-svg'
|
'qt6-svg'
|
||||||
'qt6-websockets'
|
'qt6-websockets'
|
||||||
'spdlog'
|
'spdlog'
|
||||||
@@ -29,17 +30,19 @@ makedepends=(
|
|||||||
# new repository has published versioned release tags.
|
# new repository has published versioned release tags.
|
||||||
source=("${pkgname}-${pkgver}.tar.gz")
|
source=("${pkgname}-${pkgver}.tar.gz")
|
||||||
sha256sums=('SKIP')
|
sha256sums=('SKIP')
|
||||||
|
options=('!lto')
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
cmake -S "${srcdir}/qEmby-${pkgver}" -B "${srcdir}/build" -G Ninja \
|
cmake -S "${srcdir}/qEmby-${pkgver}" -B "${srcdir}/build" -G Ninja \
|
||||||
-DCMAKE_BUILD_TYPE=None \
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||||
-DCMAKE_INSTALL_LIBDIR=lib \
|
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||||
-DQEMBY_USE_SYSTEM_DEPS=ON \
|
-DQEMBY_USE_SYSTEM_DEPS=ON \
|
||||||
-DQEMBY_FETCHCONTENT_FALLBACK=OFF \
|
-DQEMBY_FETCHCONTENT_FALLBACK=OFF \
|
||||||
-DFETCHCONTENT_FULLY_DISCONNECTED=ON \
|
-DFETCHCONTENT_FULLY_DISCONNECTED=ON \
|
||||||
-DQEMBY_NATIVE_OPTIMIZE=OFF \
|
-DQEMBY_NATIVE_OPTIMIZE=OFF \
|
||||||
-DQEMBY_ENABLE_IPO=OFF
|
-DQEMBY_ENABLE_IPO=OFF \
|
||||||
|
-DQEMBY_BUILD_NAVIGATION_TESTS=OFF
|
||||||
cmake --build "${srcdir}/build"
|
cmake --build "${srcdir}/build"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+28
-21
@@ -1,44 +1,51 @@
|
|||||||
# Arch package
|
# Arch package
|
||||||
|
|
||||||
This PKGBUILD is intended for Arch Linux local release packaging. It builds
|
从当前仓库源码打包 qEmby(使用系统 Qt6 / mpv / qcoro / spdlog)。
|
||||||
from a local source archive exported from this repository, uses Arch system
|
|
||||||
libraries, and disables FetchContent so package builds do not download
|
|
||||||
dependencies during CMake configure.
|
|
||||||
|
|
||||||
Before publishing an AUR-style release package, switch `source` to the new
|
## 构建
|
||||||
repository's versioned tag archive and replace the temporary `SKIP` checksum
|
|
||||||
with a real checksum, for example by running `updpkgsums` after the tag archive
|
|
||||||
is available.
|
|
||||||
|
|
||||||
## Build
|
```bash
|
||||||
|
|
||||||
```fish
|
|
||||||
cd packaging/arch
|
cd packaging/arch
|
||||||
git archive --format=tar.gz --prefix=qEmby-0.0.6/ -o qemby-0.0.6.tar.gz HEAD
|
|
||||||
|
# 从工作树导出源码包(含未提交改动;排除 build 产物)
|
||||||
|
VERSION=0.0.8
|
||||||
|
rm -f "qemby-${VERSION}.tar.gz"
|
||||||
|
tar -C ../.. -czf "qemby-${VERSION}.tar.gz" \
|
||||||
|
--transform "s,^,qEmby-${VERSION}/," \
|
||||||
|
--exclude='./build' \
|
||||||
|
--exclude='./cmake-build-*' \
|
||||||
|
--exclude='./packaging/arch/pkg' \
|
||||||
|
--exclude='./packaging/arch/src' \
|
||||||
|
--exclude='./packaging/arch/*.pkg.tar.zst' \
|
||||||
|
--exclude='./packaging/arch/*.tar.gz' \
|
||||||
|
--exclude='./.git' \
|
||||||
|
--exclude='./.omo' \
|
||||||
|
.
|
||||||
|
|
||||||
makepkg -f
|
makepkg -f
|
||||||
```
|
```
|
||||||
|
|
||||||
## Install
|
## 安装
|
||||||
|
|
||||||
```fish
|
```bash
|
||||||
cd packaging/arch
|
cd packaging/arch
|
||||||
sudo pacman -U qemby-*.pkg.tar.zst
|
sudo pacman -U ./qemby-0.0.8-1-x86_64.pkg.tar.zst
|
||||||
```
|
```
|
||||||
|
|
||||||
## Run
|
## 运行
|
||||||
|
|
||||||
```fish
|
```bash
|
||||||
qemby
|
qemby
|
||||||
```
|
```
|
||||||
|
|
||||||
If native Wayland causes windowing or OpenGL issues, run with the XCB fallback:
|
Wayland 异常时可强制 XCB:
|
||||||
|
|
||||||
```fish
|
```bash
|
||||||
QEMBY_FORCE_XCB=1 qemby
|
QEMBY_FORCE_XCB=1 qemby
|
||||||
```
|
```
|
||||||
|
|
||||||
## Uninstall
|
## 卸载
|
||||||
|
|
||||||
```fish
|
```bash
|
||||||
sudo pacman -R qemby
|
sudo pacman -R qemby
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# src/qEmbyApp/CMakeLists.txt
|
# src/qEmbyApp/CMakeLists.txt
|
||||||
|
|
||||||
|
# 界面语言:英文源文 + 简体中文。法语 .ts 保留仓库但不参与编译。
|
||||||
set(TS_FILES
|
set(TS_FILES
|
||||||
resources/i18n/qEmby_zh_CN.ts
|
resources/i18n/qEmby_zh_CN.ts
|
||||||
resources/i18n/qEmby_fr_FR.ts
|
|
||||||
)
|
)
|
||||||
# set(QSS_QWK_FILES ../../libs/qwindowkit/examples/mainwindow/mainwindow.qrc)
|
# set(QSS_QWK_FILES ../../libs/qwindowkit/examples/mainwindow/mainwindow.qrc)
|
||||||
# set(QRC_QWK_FILES ../../libs/qwindowkit/examples/shared/resources/shared.qrc)
|
# set(QRC_QWK_FILES ../../libs/qwindowkit/examples/shared/resources/shared.qrc)
|
||||||
@@ -35,7 +35,12 @@ qt_add_qml_module(qEmbyApp
|
|||||||
VERSION 1.0
|
VERSION 1.0
|
||||||
RESOURCE_PREFIX /qt/qml
|
RESOURCE_PREFIX /qt/qml
|
||||||
QML_FILES
|
QML_FILES
|
||||||
|
qml/AppShell.qml
|
||||||
qml/Router.qml
|
qml/Router.qml
|
||||||
|
qml/TitleBar.qml
|
||||||
|
qml/SideBar.qml
|
||||||
|
qml/components/SearchHistoryPopup.qml
|
||||||
|
qml/ManagePage.qml
|
||||||
qml/ManageDashboardPage.qml
|
qml/ManageDashboardPage.qml
|
||||||
qml/LoginPage.qml
|
qml/LoginPage.qml
|
||||||
qml/SearchPage.qml
|
qml/SearchPage.qml
|
||||||
@@ -45,6 +50,8 @@ qt_add_qml_module(qEmbyApp
|
|||||||
qml/LibraryPage.qml
|
qml/LibraryPage.qml
|
||||||
qml/DetailPage.qml
|
qml/DetailPage.qml
|
||||||
qml/SeasonPage.qml
|
qml/SeasonPage.qml
|
||||||
|
qml/PlayerPage.qml
|
||||||
|
qml/IndependentPlayerWindow.qml
|
||||||
qml/AdminUsersPage.qml
|
qml/AdminUsersPage.qml
|
||||||
qml/AdminTasksPage.qml
|
qml/AdminTasksPage.qml
|
||||||
qml/AdminLibrariesPage.qml
|
qml/AdminLibrariesPage.qml
|
||||||
@@ -52,12 +59,15 @@ qt_add_qml_module(qEmbyApp
|
|||||||
qml/AdminCollectionsPage.qml
|
qml/AdminCollectionsPage.qml
|
||||||
qml/AdminTranscodingPage.qml
|
qml/AdminTranscodingPage.qml
|
||||||
qml/EmptyState.qml
|
qml/EmptyState.qml
|
||||||
|
qml/SettingsPage.qml
|
||||||
qml/SettingsAboutPage.qml
|
qml/SettingsAboutPage.qml
|
||||||
qml/SettingsGeneralPage.qml
|
qml/SettingsGeneralPage.qml
|
||||||
qml/SettingsAppearancePage.qml
|
qml/SettingsAppearancePage.qml
|
||||||
qml/SettingsLibraryPage.qml
|
qml/SettingsLibraryPage.qml
|
||||||
qml/SettingsPlayerPage.qml
|
qml/SettingsPlayerPage.qml
|
||||||
qml/components/SettingsCard.qml
|
qml/components/SettingsCard.qml
|
||||||
|
qml/components/PageHeader.qml
|
||||||
|
qml/components/SegmentedTabs.qml
|
||||||
qml/components/CollapsibleSection.qml
|
qml/components/CollapsibleSection.qml
|
||||||
qml/components/ThemedButton.qml
|
qml/components/ThemedButton.qml
|
||||||
qml/components/ThemedSwitch.qml
|
qml/components/ThemedSwitch.qml
|
||||||
@@ -67,6 +77,7 @@ qt_add_qml_module(qEmbyApp
|
|||||||
qml/components/ThemedSlider.qml
|
qml/components/ThemedSlider.qml
|
||||||
qml/components/ThemedScrollBar.qml
|
qml/components/ThemedScrollBar.qml
|
||||||
qml/components/ThemedDialog.qml
|
qml/components/ThemedDialog.qml
|
||||||
|
qml/components/ConfirmDialog.qml
|
||||||
qml/components/ToastHost.qml
|
qml/components/ToastHost.qml
|
||||||
qml/components/LoadingState.qml
|
qml/components/LoadingState.qml
|
||||||
qml/components/ErrorState.qml
|
qml/components/ErrorState.qml
|
||||||
@@ -74,11 +85,19 @@ qt_add_qml_module(qEmbyApp
|
|||||||
qml/components/MediaGrid.qml
|
qml/components/MediaGrid.qml
|
||||||
qml/components/MediaList.qml
|
qml/components/MediaList.qml
|
||||||
qml/components/MediaSection.qml
|
qml/components/MediaSection.qml
|
||||||
|
qml/MpvSpikePage.qml
|
||||||
|
qml/MpvSpikeHarness.qml
|
||||||
SOURCES
|
SOURCES
|
||||||
quick/quicktheme.h quick/quicktheme.cpp
|
quick/quicktheme.h quick/quicktheme.cpp
|
||||||
|
quick/mpvitem.h quick/mpvitem.cpp
|
||||||
|
quick/danmakupainteditem.h quick/danmakupainteditem.cpp
|
||||||
quick/qmltask.h quick/qmltask.cpp
|
quick/qmltask.h quick/qmltask.cpp
|
||||||
quick/settingsviewmodel.h quick/settingsviewmodel.cpp
|
quick/settingsviewmodel.h quick/settingsviewmodel.cpp
|
||||||
quick/loginviewmodel.h quick/loginviewmodel.cpp
|
quick/loginviewmodel.h quick/loginviewmodel.cpp
|
||||||
|
quick/searchhistoryviewmodel.h quick/searchhistoryviewmodel.cpp
|
||||||
|
quick/sidebarviewmodel.h quick/sidebarviewmodel.cpp
|
||||||
|
quick/appshellhost.h quick/appshellhost.cpp
|
||||||
|
quick/shelllifecyclecontroller.h quick/shelllifecyclecontroller.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
# qmltyperegistrar 生成的注册代码以 <quicktheme.h> 形式包含头文件
|
# qmltyperegistrar 生成的注册代码以 <quicktheme.h> 形式包含头文件
|
||||||
@@ -148,6 +167,7 @@ target_link_libraries(qEmbyApp PRIVATE
|
|||||||
Qt${QT_VERSION_MAJOR}::QuickControls2
|
Qt${QT_VERSION_MAJOR}::QuickControls2
|
||||||
qEmbyCore
|
qEmbyCore
|
||||||
QWKWidgets
|
QWKWidgets
|
||||||
|
QWKQuick
|
||||||
WidgetFrame
|
WidgetFrame
|
||||||
QCoro6::Core
|
QCoro6::Core
|
||||||
QCoro6::Network
|
QCoro6::Network
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
#include "adminusercard.h"
|
|
||||||
#include "../utils/layoututils.h"
|
|
||||||
#include "elidedlabel.h"
|
|
||||||
#include "modernswitch.h"
|
|
||||||
|
|
||||||
#include <QCoreApplication>
|
|
||||||
#include <QDateTime>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QMouseEvent>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QTimeZone>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
|
|
||||||
QString formatIsoDateTime(QString isoDate)
|
|
||||||
{
|
|
||||||
if (isoDate.isEmpty())
|
|
||||||
{
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
QDateTime dateTime = QDateTime::fromString(isoDate, Qt::ISODate);
|
|
||||||
if (!dateTime.isValid())
|
|
||||||
{
|
|
||||||
dateTime = QDateTime::fromString(isoDate, Qt::ISODateWithMs);
|
|
||||||
}
|
|
||||||
if (!dateTime.isValid())
|
|
||||||
{
|
|
||||||
return isoDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
dateTime.setTimeZone(QTimeZone::UTC);
|
|
||||||
return dateTime.toLocalTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss"));
|
|
||||||
}
|
|
||||||
|
|
||||||
QString displayNameForUser(const UserInfo &user)
|
|
||||||
{
|
|
||||||
const QString trimmedName = user.name.trimmed();
|
|
||||||
if (!trimmedName.isEmpty())
|
|
||||||
{
|
|
||||||
return trimmedName;
|
|
||||||
}
|
|
||||||
return user.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString pageUsersText(const char *sourceText)
|
|
||||||
{
|
|
||||||
return QCoreApplication::translate("PageUsers", sourceText);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setPassiveClickThrough(QWidget *widget)
|
|
||||||
{
|
|
||||||
if (!widget)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
widget->setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
AdminUserCard::AdminUserCard(UserInfo userInfo, QString currentUserId, QWidget *parent)
|
|
||||||
: QFrame(parent), m_user(userInfo), m_currentUserId(currentUserId), m_displayName(displayNameForUser(m_user))
|
|
||||||
{
|
|
||||||
static constexpr int CardWidth = 268;
|
|
||||||
static constexpr int CardHeight = 196;
|
|
||||||
|
|
||||||
setObjectName("ManageUserCard");
|
|
||||||
setAttribute(Qt::WA_StyledBackground, true);
|
|
||||||
setAttribute(Qt::WA_Hover, true);
|
|
||||||
setMouseTracking(true);
|
|
||||||
setCursor(Qt::PointingHandCursor);
|
|
||||||
setToolTip(m_displayName);
|
|
||||||
setFixedSize(CardWidth, CardHeight);
|
|
||||||
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
||||||
|
|
||||||
auto *cardLayout = new QVBoxLayout(this);
|
|
||||||
cardLayout->setContentsMargins(14, 12, 14, 12);
|
|
||||||
cardLayout->setSpacing(8);
|
|
||||||
|
|
||||||
auto *topRow = new QHBoxLayout();
|
|
||||||
topRow->setContentsMargins(0, 0, 0, 0);
|
|
||||||
topRow->setSpacing(10);
|
|
||||||
|
|
||||||
auto *avatarLabel = new QLabel(this);
|
|
||||||
avatarLabel->setObjectName("ManageUserAvatar");
|
|
||||||
avatarLabel->setFixedSize(48, 48);
|
|
||||||
avatarLabel->setAlignment(Qt::AlignCenter);
|
|
||||||
avatarLabel->setText(m_displayName.isEmpty() ? QStringLiteral("?") : m_displayName.left(1).toUpper());
|
|
||||||
setPassiveClickThrough(avatarLabel);
|
|
||||||
topRow->addWidget(avatarLabel, 0, Qt::AlignTop);
|
|
||||||
|
|
||||||
auto *titleColumn = new QVBoxLayout();
|
|
||||||
titleColumn->setContentsMargins(0, 0, 0, 0);
|
|
||||||
titleColumn->setSpacing(4);
|
|
||||||
|
|
||||||
auto *nameLabel = new ElidedLabel(this);
|
|
||||||
nameLabel->setObjectName("ManageCardTitle");
|
|
||||||
nameLabel->setFullText(m_displayName);
|
|
||||||
setPassiveClickThrough(nameLabel);
|
|
||||||
titleColumn->addWidget(nameLabel);
|
|
||||||
|
|
||||||
auto *badgeRow = new QHBoxLayout();
|
|
||||||
badgeRow->setContentsMargins(0, 0, 0, 0);
|
|
||||||
badgeRow->setSpacing(6);
|
|
||||||
|
|
||||||
if (m_user.isAdministrator)
|
|
||||||
{
|
|
||||||
auto *adminBadge = new QLabel(pageUsersText("Admin"), this);
|
|
||||||
adminBadge->setObjectName("ManageUserBadgeAdmin");
|
|
||||||
setPassiveClickThrough(adminBadge);
|
|
||||||
badgeRow->addWidget(adminBadge);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_user.isDisabled)
|
|
||||||
{
|
|
||||||
auto *disabledBadge = new QLabel(pageUsersText("Disabled"), this);
|
|
||||||
disabledBadge->setObjectName("ManageUserBadgeDisabled");
|
|
||||||
setPassiveClickThrough(disabledBadge);
|
|
||||||
badgeRow->addWidget(disabledBadge);
|
|
||||||
}
|
|
||||||
|
|
||||||
badgeRow->addStretch(1);
|
|
||||||
titleColumn->addLayout(badgeRow);
|
|
||||||
topRow->addLayout(titleColumn, 1);
|
|
||||||
|
|
||||||
cardLayout->addLayout(topRow);
|
|
||||||
|
|
||||||
auto *separator = LayoutUtils::createSeparator(this);
|
|
||||||
setPassiveClickThrough(separator);
|
|
||||||
cardLayout->addWidget(separator);
|
|
||||||
|
|
||||||
const QString lastLoginText = formatIsoDateTime(m_user.lastLoginDate);
|
|
||||||
auto *lastLoginLabel = new QLabel(
|
|
||||||
pageUsersText("Last login: %1").arg(lastLoginText.isEmpty() ? QStringLiteral("-") : lastLoginText), this);
|
|
||||||
lastLoginLabel->setObjectName("ManageSessionClient");
|
|
||||||
lastLoginLabel->setWordWrap(true);
|
|
||||||
setPassiveClickThrough(lastLoginLabel);
|
|
||||||
cardLayout->addWidget(lastLoginLabel);
|
|
||||||
|
|
||||||
const QString lastActivityText = formatIsoDateTime(m_user.lastActivityDate);
|
|
||||||
auto *lastActivityLabel = new QLabel(
|
|
||||||
pageUsersText("Last active: %1").arg(lastActivityText.isEmpty() ? QStringLiteral("-") : lastActivityText),
|
|
||||||
this);
|
|
||||||
lastActivityLabel->setObjectName("ManageSessionTime");
|
|
||||||
lastActivityLabel->setWordWrap(true);
|
|
||||||
setPassiveClickThrough(lastActivityLabel);
|
|
||||||
cardLayout->addWidget(lastActivityLabel);
|
|
||||||
|
|
||||||
cardLayout->addStretch(1);
|
|
||||||
|
|
||||||
auto *footerRow = new QHBoxLayout();
|
|
||||||
footerRow->setContentsMargins(0, 0, 0, 0);
|
|
||||||
footerRow->setSpacing(8);
|
|
||||||
footerRow->addStretch(1);
|
|
||||||
|
|
||||||
auto *switchWidget = new ModernSwitch(this);
|
|
||||||
switchWidget->setChecked(!m_user.isDisabled);
|
|
||||||
switchWidget->setToolTip(m_user.isDisabled ? pageUsersText("Enable user") : pageUsersText("Disable user"));
|
|
||||||
if (m_user.id == m_currentUserId)
|
|
||||||
{
|
|
||||||
switchWidget->setEnabled(false);
|
|
||||||
switchWidget->setToolTip(pageUsersText("Cannot disable yourself"));
|
|
||||||
}
|
|
||||||
connect(switchWidget, &ModernSwitch::toggled, this,
|
|
||||||
[this, switchWidget](bool checked)
|
|
||||||
{
|
|
||||||
switchWidget->setToolTip(checked ? pageUsersText("Disable user") : pageUsersText("Enable user"));
|
|
||||||
emit toggleRequested(m_user.id, m_displayName, checked);
|
|
||||||
});
|
|
||||||
footerRow->addWidget(switchWidget, 0, Qt::AlignVCenter);
|
|
||||||
|
|
||||||
auto *deleteBtn = new QPushButton(this);
|
|
||||||
deleteBtn->setObjectName("CollCardDeleteBtn");
|
|
||||||
deleteBtn->setFixedSize(24, 24);
|
|
||||||
deleteBtn->setCursor(Qt::PointingHandCursor);
|
|
||||||
deleteBtn->setToolTip(pageUsersText("Delete user"));
|
|
||||||
if (m_user.id == m_currentUserId)
|
|
||||||
{
|
|
||||||
deleteBtn->setEnabled(false);
|
|
||||||
deleteBtn->setToolTip(pageUsersText("Cannot delete yourself"));
|
|
||||||
}
|
|
||||||
connect(deleteBtn, &QPushButton::clicked, this, [this]() { emit deleteRequested(m_user.id, m_displayName); });
|
|
||||||
footerRow->addWidget(deleteBtn, 0, Qt::AlignVCenter);
|
|
||||||
|
|
||||||
cardLayout->addLayout(footerRow);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AdminUserCard::mouseReleaseEvent(QMouseEvent *event)
|
|
||||||
{
|
|
||||||
if (event->button() == Qt::LeftButton)
|
|
||||||
{
|
|
||||||
emit clicked(m_user.id);
|
|
||||||
event->accept();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFrame::mouseReleaseEvent(event);
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
#ifndef ADMINUSERCARD_H
|
|
||||||
#define ADMINUSERCARD_H
|
|
||||||
|
|
||||||
#include <QFrame>
|
|
||||||
#include <QString>
|
|
||||||
#include <models/admin/adminmodels.h>
|
|
||||||
|
|
||||||
class QMouseEvent;
|
|
||||||
|
|
||||||
class AdminUserCard : public QFrame {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit AdminUserCard(UserInfo userInfo,
|
|
||||||
QString currentUserId,
|
|
||||||
QWidget* parent = nullptr);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void clicked(QString userId);
|
|
||||||
void toggleRequested(QString userId, QString userName, bool enable);
|
|
||||||
void deleteRequested(QString userId, QString userName);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void mouseReleaseEvent(QMouseEvent* event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
UserInfo m_user;
|
|
||||||
QString m_currentUserId;
|
|
||||||
QString m_displayName;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,342 +0,0 @@
|
|||||||
#include "collectioncard.h"
|
|
||||||
#include "collectiongrid.h"
|
|
||||||
#include "../managers/thememanager.h"
|
|
||||||
#include "../tokens/ColorTokens.h"
|
|
||||||
#include "../utils/hoveranimator.h"
|
|
||||||
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QMouseEvent>
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QPainterPath>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
QColor lerpColor(const QColor& from, const QColor& to, qreal t)
|
|
||||||
{
|
|
||||||
return QColor(qRound(from.red() + (to.red() - from.red()) * t),
|
|
||||||
qRound(from.green() + (to.green() - from.green()) * t),
|
|
||||||
qRound(from.blue() + (to.blue() - from.blue()) * t),
|
|
||||||
qRound(from.alpha() + (to.alpha() - from.alpha()) * t));
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CollectionCard::CollectionCard(const QString& itemId,
|
|
||||||
const QString& name,
|
|
||||||
int childCount,
|
|
||||||
const QString& type,
|
|
||||||
QWidget* parent)
|
|
||||||
: QFrame(parent)
|
|
||||||
, m_itemId(itemId)
|
|
||||||
, m_name(name)
|
|
||||||
, m_childCount(childCount)
|
|
||||||
, m_type(type)
|
|
||||||
{
|
|
||||||
setFixedSize(CollectionGrid::CardWidth, CollectionGrid::CardHeight);
|
|
||||||
setMouseTracking(true);
|
|
||||||
setCursor(Qt::OpenHandCursor);
|
|
||||||
|
|
||||||
|
|
||||||
auto makeBtn = [this](const QString& objName) {
|
|
||||||
auto* btn = new QPushButton(this);
|
|
||||||
btn->setObjectName(objName);
|
|
||||||
btn->setFixedSize(24, 24);
|
|
||||||
btn->setCursor(Qt::PointingHandCursor);
|
|
||||||
return btn;
|
|
||||||
};
|
|
||||||
|
|
||||||
m_btnRename = makeBtn("CollCardRenameBtn");
|
|
||||||
m_btnDelete = makeBtn("CollCardDeleteBtn");
|
|
||||||
|
|
||||||
m_btnRename->setToolTip(tr("Rename"));
|
|
||||||
m_btnDelete->setToolTip(tr("Delete"));
|
|
||||||
|
|
||||||
m_hover = new HoverAnimator(this);
|
|
||||||
m_hover->setCallback([this](qreal) { update(); });
|
|
||||||
|
|
||||||
updateButtonLayout();
|
|
||||||
|
|
||||||
connect(m_btnRename, &QPushButton::clicked, this,
|
|
||||||
[this]() { emit renameClicked(m_itemId, m_name); });
|
|
||||||
connect(m_btnDelete, &QPushButton::clicked, this,
|
|
||||||
[this]() { emit deleteClicked(m_itemId, m_name); });
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionCard::updateButtonLayout()
|
|
||||||
{
|
|
||||||
const int buttonSize = 24;
|
|
||||||
const int buttonSpacing = 4;
|
|
||||||
const int totalWidth = 2 * buttonSize + buttonSpacing;
|
|
||||||
const int startX = width() - totalWidth - 8;
|
|
||||||
const int buttonY = height() - buttonSize - 6;
|
|
||||||
|
|
||||||
m_btnRename->move(startX, buttonY);
|
|
||||||
m_btnDelete->move(startX + buttonSize + buttonSpacing, buttonY);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionCard::setImage(const QPixmap& pixmap)
|
|
||||||
{
|
|
||||||
m_image = pixmap;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionCard::setDisplayData(const QString& name, int childCount,
|
|
||||||
const QString& type)
|
|
||||||
{
|
|
||||||
if (m_name == name && m_childCount == childCount && m_type == type) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_name = name;
|
|
||||||
m_childCount = childCount;
|
|
||||||
m_type = type;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionCard::setSelected(bool selected)
|
|
||||||
{
|
|
||||||
if (m_selected == selected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_selected = selected;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionCard::setDragging(bool dragging)
|
|
||||||
{
|
|
||||||
if (m_dragging == dragging) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_dragging = dragging;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionCard::paintEvent(QPaintEvent* event)
|
|
||||||
{
|
|
||||||
Q_UNUSED(event)
|
|
||||||
QPainter p(this);
|
|
||||||
p.setRenderHint(QPainter::Antialiasing, true);
|
|
||||||
|
|
||||||
bool isDark = ThemeManager::instance()->isDarkMode();
|
|
||||||
int w = width(), h = height();
|
|
||||||
|
|
||||||
|
|
||||||
using namespace ColorTokens;
|
|
||||||
QColor cardBg = isDark ? withAlpha(Slate::k700, 140) : withAlpha(Qt::white, 245);
|
|
||||||
QColor border = isDark ? withAlpha(Slate::k600, 100) : Slate::k200;
|
|
||||||
QColor nameFg = isDark ? Slate::k100 : Slate::k900;
|
|
||||||
QColor typeFg = isDark ? Slate::k400 : Slate::k500;
|
|
||||||
const QColor accent = Blue::k500;
|
|
||||||
|
|
||||||
const qreal hp = m_hover->progress();
|
|
||||||
if (hp > 0.0 && !m_selected && !m_dragging) {
|
|
||||||
const QColor hoverBorder = withAlpha(isDark ? Blue::k400 : Blue::k500, 190);
|
|
||||||
const QColor hoverBg = isDark ? withAlpha(Slate::k700, 190) : QColor(Qt::white);
|
|
||||||
border = lerpColor(border, hoverBorder, hp);
|
|
||||||
cardBg = lerpColor(cardBg, hoverBg, hp);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_selected) {
|
|
||||||
cardBg = isDark ? withAlpha(Slate::k800, 220) : withAlpha(Blue::k50, 245);
|
|
||||||
border = withAlpha(isDark ? Blue::k400 : Blue::k500, 220);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_dragging) {
|
|
||||||
cardBg = isDark ? withAlpha(Slate::k900, 235) : withAlpha(Slate::k50, 252);
|
|
||||||
border = isDark ? withAlpha(Blue::k500, 235) : withAlpha(Blue::k600, 230);
|
|
||||||
}
|
|
||||||
|
|
||||||
p.setPen(QPen(border, m_selected ? 2 : 1));
|
|
||||||
p.setBrush(cardBg);
|
|
||||||
p.drawRoundedRect(QRectF(0.5, 0.5, w - 1, h - 1), 12, 12);
|
|
||||||
|
|
||||||
const QRectF badgeRect(w - 28, 10, 18, 18);
|
|
||||||
p.setPen(QPen(m_selected ? accent
|
|
||||||
: (isDark ? withAlpha(Slate::k500, 160)
|
|
||||||
: withAlpha(Slate::k400, 180)),
|
|
||||||
1.5));
|
|
||||||
p.setBrush(m_selected ? accent
|
|
||||||
: (isDark ? withAlpha(Slate::k900, 180)
|
|
||||||
: withAlpha(Qt::white, 220)));
|
|
||||||
p.drawEllipse(badgeRect);
|
|
||||||
|
|
||||||
if (m_selected) {
|
|
||||||
QPainterPath checkPath;
|
|
||||||
checkPath.moveTo(badgeRect.left() + 4.5, badgeRect.center().y());
|
|
||||||
checkPath.lineTo(badgeRect.left() + 7.5, badgeRect.bottom() - 4.5);
|
|
||||||
checkPath.lineTo(badgeRect.right() - 4.0, badgeRect.top() + 4.5);
|
|
||||||
p.setPen(QPen(Qt::white, 2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
|
|
||||||
p.drawPath(checkPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const int imagePadding = 4;
|
|
||||||
const int defaultImgAreaH = 120;
|
|
||||||
const bool useSquareThumb = (m_type == "Playlist");
|
|
||||||
const int imgSide = qMax(0, w - imagePadding * 2);
|
|
||||||
QRectF imgRect(imagePadding, imagePadding,
|
|
||||||
w - imagePadding * 2,
|
|
||||||
useSquareThumb ? imgSide : defaultImgAreaH);
|
|
||||||
|
|
||||||
QLinearGradient grad(imgRect.topLeft(), imgRect.bottomRight());
|
|
||||||
grad.setColorAt(0, isDark ? Slate::k800 : Slate::k100);
|
|
||||||
grad.setColorAt(1, isDark ? Slate::k700 : Slate::k200);
|
|
||||||
|
|
||||||
QPainterPath clipPath;
|
|
||||||
clipPath.addRoundedRect(imgRect, 9, 9);
|
|
||||||
p.save();
|
|
||||||
p.setClipPath(clipPath);
|
|
||||||
p.fillRect(imgRect, grad);
|
|
||||||
|
|
||||||
if (!m_image.isNull()) {
|
|
||||||
const Qt::AspectRatioMode aspectMode =
|
|
||||||
useSquareThumb ? Qt::KeepAspectRatio : Qt::KeepAspectRatioByExpanding;
|
|
||||||
QPixmap scaled = m_image.scaled(imgRect.size().toSize(), aspectMode,
|
|
||||||
Qt::SmoothTransformation);
|
|
||||||
|
|
||||||
const qreal scaledDpr = scaled.devicePixelRatio();
|
|
||||||
QSize scaledSize(qRound(scaled.width() / scaledDpr),
|
|
||||||
qRound(scaled.height() / scaledDpr));
|
|
||||||
QRect drawRect(0, 0, scaledSize.width(), scaledSize.height());
|
|
||||||
drawRect.moveLeft(qRound(imgRect.x() + (imgRect.width() - drawRect.width()) / 2.0));
|
|
||||||
drawRect.moveTop(qRound(imgRect.y() + (imgRect.height() - drawRect.height()) / 2.0));
|
|
||||||
|
|
||||||
p.drawPixmap(drawRect.topLeft(), scaled);
|
|
||||||
} else {
|
|
||||||
QFont iconFont = font();
|
|
||||||
iconFont.setPixelSize(32);
|
|
||||||
p.setFont(iconFont);
|
|
||||||
p.setPen(withAlpha(isDark ? Slate::k400 : Slate::k500, 80));
|
|
||||||
|
|
||||||
QString placeholder = (m_type == "Playlist") ? QString::fromUtf8("🎵") : QString::fromUtf8("📂");
|
|
||||||
p.drawText(imgRect, Qt::AlignCenter, placeholder);
|
|
||||||
}
|
|
||||||
p.restore();
|
|
||||||
|
|
||||||
|
|
||||||
int textTop = qRound(imgRect.bottom()) + 8;
|
|
||||||
int textLeft = 10;
|
|
||||||
int textWidth = w - 20;
|
|
||||||
|
|
||||||
QFont nameFont = font();
|
|
||||||
nameFont.setPixelSize(13);
|
|
||||||
nameFont.setWeight(QFont::DemiBold);
|
|
||||||
nameFont.setFamilies({"Segoe UI", "Microsoft YaHei", "sans-serif"});
|
|
||||||
QFontMetrics nameFm(nameFont);
|
|
||||||
QString elidedName = nameFm.elidedText(m_name, Qt::ElideRight, textWidth);
|
|
||||||
|
|
||||||
p.setFont(nameFont);
|
|
||||||
p.setPen(nameFg);
|
|
||||||
p.drawText(QRect(textLeft, textTop, textWidth, 18), Qt::AlignLeft | Qt::AlignVCenter, elidedName);
|
|
||||||
|
|
||||||
QFont typeFont = font();
|
|
||||||
typeFont.setPixelSize(11);
|
|
||||||
typeFont.setFamilies({"Segoe UI", "Microsoft YaHei", "sans-serif"});
|
|
||||||
|
|
||||||
QString typeStr;
|
|
||||||
if (m_type == "Playlist") {
|
|
||||||
typeStr = tr("Playlist");
|
|
||||||
} else {
|
|
||||||
typeStr = tr("Collection");
|
|
||||||
}
|
|
||||||
typeStr += QString(" · %1 %2").arg(m_childCount)
|
|
||||||
.arg(m_childCount == 1 ? tr("item") : tr("items"));
|
|
||||||
|
|
||||||
p.setFont(typeFont);
|
|
||||||
p.setPen(typeFg);
|
|
||||||
p.drawText(QRect(textLeft, textTop + 20, textWidth, 16), Qt::AlignLeft | Qt::AlignVCenter, typeStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionCard::mousePressEvent(QMouseEvent* event)
|
|
||||||
{
|
|
||||||
if (event->button() == Qt::LeftButton) {
|
|
||||||
if (!qobject_cast<QPushButton*>(childAt(event->position().toPoint()))) {
|
|
||||||
m_pressCandidate = true;
|
|
||||||
m_dragStartPos = event->globalPosition().toPoint();
|
|
||||||
setCursor(Qt::OpenHandCursor);
|
|
||||||
event->accept();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QFrame::mousePressEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionCard::mouseMoveEvent(QMouseEvent* event)
|
|
||||||
{
|
|
||||||
if ((event->buttons() & Qt::LeftButton) && m_pressCandidate) {
|
|
||||||
const QPoint globalPos = event->globalPosition().toPoint();
|
|
||||||
if (!m_dragging) {
|
|
||||||
const int distance = (globalPos - m_dragStartPos).manhattanLength();
|
|
||||||
if (distance >= QApplication::startDragDistance()) {
|
|
||||||
setDragging(true);
|
|
||||||
setCursor(Qt::ClosedHandCursor);
|
|
||||||
if (auto* grid = qobject_cast<CollectionGrid*>(parentWidget())) {
|
|
||||||
grid->onCardDragStart(this, globalPos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_dragging) {
|
|
||||||
if (auto* grid = qobject_cast<CollectionGrid*>(parentWidget())) {
|
|
||||||
grid->onCardDragMove(this, globalPos);
|
|
||||||
}
|
|
||||||
event->accept();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QFrame::mouseMoveEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionCard::mouseReleaseEvent(QMouseEvent* event)
|
|
||||||
{
|
|
||||||
if (event->button() == Qt::LeftButton && m_pressCandidate) {
|
|
||||||
const bool wasDragging = m_dragging;
|
|
||||||
m_pressCandidate = false;
|
|
||||||
|
|
||||||
if (wasDragging) {
|
|
||||||
setDragging(false);
|
|
||||||
setCursor(Qt::OpenHandCursor);
|
|
||||||
if (auto* grid = qobject_cast<CollectionGrid*>(parentWidget())) {
|
|
||||||
grid->onCardDragEnd(this);
|
|
||||||
}
|
|
||||||
event->accept();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleSelected();
|
|
||||||
event->accept();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFrame::mouseReleaseEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionCard::resizeEvent(QResizeEvent* event)
|
|
||||||
{
|
|
||||||
QFrame::resizeEvent(event);
|
|
||||||
updateButtonLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionCard::enterEvent(QEnterEvent* event)
|
|
||||||
{
|
|
||||||
QFrame::enterEvent(event);
|
|
||||||
m_hover->setHovered(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionCard::leaveEvent(QEvent* event)
|
|
||||||
{
|
|
||||||
QFrame::leaveEvent(event);
|
|
||||||
m_hover->setHovered(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionCard::toggleSelected()
|
|
||||||
{
|
|
||||||
m_selected = !m_selected;
|
|
||||||
update();
|
|
||||||
emit selectionChanged(m_itemId, m_selected);
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
#ifndef COLLECTIONCARD_H
|
|
||||||
#define COLLECTIONCARD_H
|
|
||||||
|
|
||||||
#include <QFrame>
|
|
||||||
#include <QPoint>
|
|
||||||
#include <QPixmap>
|
|
||||||
#include <QPushButton>
|
|
||||||
|
|
||||||
class CollectionGrid;
|
|
||||||
class HoverAnimator;
|
|
||||||
class QEnterEvent;
|
|
||||||
class QMouseEvent;
|
|
||||||
class QResizeEvent;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CollectionCard : public QFrame {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit CollectionCard(const QString& itemId,
|
|
||||||
const QString& name,
|
|
||||||
int childCount,
|
|
||||||
const QString& type,
|
|
||||||
QWidget* parent = nullptr);
|
|
||||||
|
|
||||||
QString itemId() const { return m_itemId; }
|
|
||||||
QString itemName() const { return m_name; }
|
|
||||||
bool hasImage() const { return !m_image.isNull(); }
|
|
||||||
bool isSelected() const { return m_selected; }
|
|
||||||
bool isDragging() const { return m_dragging; }
|
|
||||||
void setImage(const QPixmap& pixmap);
|
|
||||||
void setDisplayData(const QString& name, int childCount, const QString& type);
|
|
||||||
void setSelected(bool selected);
|
|
||||||
void setDragging(bool dragging);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void renameClicked(const QString& id, const QString& name);
|
|
||||||
void deleteClicked(const QString& id, const QString& name);
|
|
||||||
void selectionChanged(const QString& id, bool selected);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void paintEvent(QPaintEvent* event) override;
|
|
||||||
void mousePressEvent(QMouseEvent* event) override;
|
|
||||||
void mouseMoveEvent(QMouseEvent* event) override;
|
|
||||||
void mouseReleaseEvent(QMouseEvent* event) override;
|
|
||||||
void resizeEvent(QResizeEvent* event) override;
|
|
||||||
void enterEvent(QEnterEvent* event) override;
|
|
||||||
void leaveEvent(QEvent* event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void updateButtonLayout();
|
|
||||||
void toggleSelected();
|
|
||||||
|
|
||||||
QString m_itemId;
|
|
||||||
QString m_name;
|
|
||||||
int m_childCount;
|
|
||||||
QString m_type;
|
|
||||||
QPixmap m_image;
|
|
||||||
bool m_selected = false;
|
|
||||||
bool m_dragging = false;
|
|
||||||
bool m_pressCandidate = false;
|
|
||||||
QPoint m_dragStartPos;
|
|
||||||
|
|
||||||
QPushButton* m_btnRename;
|
|
||||||
QPushButton* m_btnDelete;
|
|
||||||
HoverAnimator* m_hover = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,341 +0,0 @@
|
|||||||
#include "collectiongrid.h"
|
|
||||||
#include "collectioncard.h"
|
|
||||||
|
|
||||||
#include <QPropertyAnimation>
|
|
||||||
#include <QResizeEvent>
|
|
||||||
#include <QScrollArea>
|
|
||||||
#include <QScrollBar>
|
|
||||||
#include <QSet>
|
|
||||||
#include <QTimer>
|
|
||||||
#include "../tokens/AnimationTokens.h"
|
|
||||||
#include "../utils/motionprefs.h"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
void stopPosAnimations(CollectionCard* card)
|
|
||||||
{
|
|
||||||
const auto children = card->children();
|
|
||||||
for (auto* child : children) {
|
|
||||||
if (auto* anim = qobject_cast<QPropertyAnimation*>(child)) {
|
|
||||||
if (anim->propertyName() == "pos") {
|
|
||||||
anim->stop();
|
|
||||||
anim->deleteLater();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CollectionGrid::CollectionGrid(QWidget* parent)
|
|
||||||
: QWidget(parent)
|
|
||||||
{
|
|
||||||
setMinimumHeight(100);
|
|
||||||
|
|
||||||
m_autoScrollTimer = new QTimer(this);
|
|
||||||
m_autoScrollTimer->setInterval(16);
|
|
||||||
connect(m_autoScrollTimer, &QTimer::timeout, this, [this]() {
|
|
||||||
if (!m_dragCard) {
|
|
||||||
m_autoScrollTimer->stop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* scrollArea = parentScrollArea();
|
|
||||||
if (!scrollArea) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* scrollBar = scrollArea->verticalScrollBar();
|
|
||||||
const int delta = autoScrollDelta(m_lastDragGlobalPos);
|
|
||||||
if (delta == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int oldValue = scrollBar->value();
|
|
||||||
scrollBar->setValue(
|
|
||||||
qBound(scrollBar->minimum(), oldValue + delta, scrollBar->maximum()));
|
|
||||||
if (scrollBar->value() != oldValue) {
|
|
||||||
updateDragPosition(m_dragCard, m_lastDragGlobalPos);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
int CollectionGrid::cardHeightForWidth(int width) const
|
|
||||||
{
|
|
||||||
const int imageSide = qMax(0, width - 8);
|
|
||||||
const int footerHeight = 84;
|
|
||||||
return qMax(CardHeight, imageSide + footerHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionGrid::setCards(const QList<CollectionCard*>& cards)
|
|
||||||
{
|
|
||||||
m_dragCard = nullptr;
|
|
||||||
m_dragOriginalIndex = -1;
|
|
||||||
m_dragCurrentIndex = -1;
|
|
||||||
if (m_autoScrollTimer) {
|
|
||||||
m_autoScrollTimer->stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
QSet<CollectionCard*> retainedCards;
|
|
||||||
retainedCards.reserve(cards.size());
|
|
||||||
for (auto* card : cards) {
|
|
||||||
retainedCards.insert(card);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto* card : m_cards) {
|
|
||||||
if (!retainedCards.contains(card)) {
|
|
||||||
card->hide();
|
|
||||||
card->deleteLater();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_cards = cards;
|
|
||||||
for (auto* card : m_cards) {
|
|
||||||
card->setParent(this);
|
|
||||||
card->show();
|
|
||||||
}
|
|
||||||
relayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
int CollectionGrid::columnsForWidth(int w) const
|
|
||||||
{
|
|
||||||
if (w <= 0) return 1;
|
|
||||||
const int cw = qMax(0, w - LeftPadding - RightPadding);
|
|
||||||
int cols = (cw + CardSpacing) / (CardWidth + CardSpacing);
|
|
||||||
return qMax(1, cols);
|
|
||||||
}
|
|
||||||
|
|
||||||
int CollectionGrid::contentWidth() const
|
|
||||||
{
|
|
||||||
return qMax(0, width() - LeftPadding - RightPadding);
|
|
||||||
}
|
|
||||||
|
|
||||||
int CollectionGrid::cardWidthForColumn(int column, int columns) const
|
|
||||||
{
|
|
||||||
if (columns <= 0) return CardWidth;
|
|
||||||
|
|
||||||
const int availableWidth =
|
|
||||||
qMax(0, contentWidth() - (columns - 1) * CardSpacing);
|
|
||||||
const int baseWidth = qMax(1, availableWidth / columns);
|
|
||||||
const int extraPixels = qMax(0, availableWidth % columns);
|
|
||||||
return baseWidth + (column < extraPixels ? 1 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
int CollectionGrid::columnX(int column, int columns) const
|
|
||||||
{
|
|
||||||
int x = LeftPadding;
|
|
||||||
for (int i = 0; i < column; ++i) {
|
|
||||||
x += cardWidthForColumn(i, columns) + CardSpacing;
|
|
||||||
}
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
|
|
||||||
int CollectionGrid::columnForX(int x, int columns) const
|
|
||||||
{
|
|
||||||
if (columns <= 0) return 0;
|
|
||||||
|
|
||||||
for (int col = 0; col < columns; ++col) {
|
|
||||||
const int rightEdge = columnX(col, columns) + cardWidthForColumn(col, columns);
|
|
||||||
if (col == columns - 1 || x < rightEdge + CardSpacing / 2) {
|
|
||||||
return col;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return columns - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPoint CollectionGrid::posForIndex(int index, int columns) const
|
|
||||||
{
|
|
||||||
if (columns <= 0) columns = 1;
|
|
||||||
|
|
||||||
const int col = index % columns;
|
|
||||||
const int row = index / columns;
|
|
||||||
return QPoint(columnX(col, columns), row * (m_rowHeight + CardSpacing));
|
|
||||||
}
|
|
||||||
|
|
||||||
int CollectionGrid::indexForPos(const QPoint& pos, int columns) const
|
|
||||||
{
|
|
||||||
if (m_cards.isEmpty()) return 0;
|
|
||||||
|
|
||||||
const int col = columnForX(pos.x(), columns);
|
|
||||||
const int row = qMax(0, pos.y() / (m_rowHeight + CardSpacing));
|
|
||||||
const int idx = row * columns + col;
|
|
||||||
return qBound(0, idx, m_cards.size() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
QScrollArea* CollectionGrid::parentScrollArea() const
|
|
||||||
{
|
|
||||||
QWidget* current = parentWidget();
|
|
||||||
while (current) {
|
|
||||||
if (auto* scrollArea = qobject_cast<QScrollArea*>(current)) {
|
|
||||||
return scrollArea;
|
|
||||||
}
|
|
||||||
current = current->parentWidget();
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
int CollectionGrid::autoScrollDelta(const QPoint& globalPos) const
|
|
||||||
{
|
|
||||||
auto* scrollArea = parentScrollArea();
|
|
||||||
if (!scrollArea) return 0;
|
|
||||||
|
|
||||||
auto* scrollBar = scrollArea->verticalScrollBar();
|
|
||||||
if (!scrollBar || scrollBar->maximum() <= scrollBar->minimum()) return 0;
|
|
||||||
|
|
||||||
const QRect viewportRect(scrollArea->viewport()->mapToGlobal(QPoint(0, 0)),
|
|
||||||
scrollArea->viewport()->size());
|
|
||||||
static constexpr int EdgeThreshold = 56;
|
|
||||||
static constexpr int MaxScrollStep = 22;
|
|
||||||
|
|
||||||
const int topHotZone = viewportRect.top() + EdgeThreshold;
|
|
||||||
if (globalPos.y() < topHotZone) {
|
|
||||||
const int distance = topHotZone - globalPos.y();
|
|
||||||
return -qBound(4, distance / 2, MaxScrollStep);
|
|
||||||
}
|
|
||||||
|
|
||||||
const int bottomHotZone = viewportRect.bottom() - EdgeThreshold;
|
|
||||||
if (globalPos.y() > bottomHotZone) {
|
|
||||||
const int distance = globalPos.y() - bottomHotZone;
|
|
||||||
return qBound(4, distance / 2, MaxScrollStep);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionGrid::updateDragPosition(CollectionCard* card, const QPoint& globalPos)
|
|
||||||
{
|
|
||||||
QPoint localPos =
|
|
||||||
mapFromGlobal(globalPos) - QPoint(card->width() / 2, card->height() / 2);
|
|
||||||
card->move(localPos);
|
|
||||||
|
|
||||||
const QPoint center = mapFromGlobal(globalPos);
|
|
||||||
int targetIndex = indexForPos(center, m_columns);
|
|
||||||
|
|
||||||
if (targetIndex != m_dragCurrentIndex &&
|
|
||||||
targetIndex >= 0 && targetIndex < m_cards.size()) {
|
|
||||||
m_cards.removeOne(card);
|
|
||||||
targetIndex = qBound(0, targetIndex, m_cards.size());
|
|
||||||
m_cards.insert(targetIndex, card);
|
|
||||||
m_dragCurrentIndex = targetIndex;
|
|
||||||
relayout(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionGrid::relayout(bool animate)
|
|
||||||
{
|
|
||||||
m_columns = columnsForWidth(width());
|
|
||||||
const int availableWidth =
|
|
||||||
qMax(0, contentWidth() - (m_columns - 1) * CardSpacing);
|
|
||||||
const int baseWidth = qMax(1, availableWidth / m_columns);
|
|
||||||
const int extraPixels = qMax(0, availableWidth % m_columns);
|
|
||||||
const int maxCardWidth = baseWidth + (extraPixels > 0 ? 1 : 0);
|
|
||||||
|
|
||||||
m_rowHeight = cardHeightForWidth(maxCardWidth);
|
|
||||||
|
|
||||||
int rows = m_cards.isEmpty() ? 0 : (m_cards.size() + m_columns - 1) / m_columns;
|
|
||||||
int totalH = rows * (m_rowHeight + CardSpacing) - (rows > 0 ? CardSpacing : 0);
|
|
||||||
setMinimumHeight(qMax(totalH, 100));
|
|
||||||
|
|
||||||
for (int i = 0; i < m_cards.size(); ++i) {
|
|
||||||
auto* card = m_cards[i];
|
|
||||||
const int col = i % m_columns;
|
|
||||||
const int cardW = baseWidth + (col < extraPixels ? 1 : 0);
|
|
||||||
card->setFixedSize(cardW, m_rowHeight);
|
|
||||||
if (card == m_dragCard) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPoint target = posForIndex(i, m_columns);
|
|
||||||
if (animate && !MotionPrefs::reduceMotion() && card->pos() != target) {
|
|
||||||
stopPosAnimations(card);
|
|
||||||
|
|
||||||
auto* anim = new QPropertyAnimation(card, "pos", card);
|
|
||||||
anim->setDuration(AnimationTokens::DurationMs::kNormal);
|
|
||||||
anim->setEasingCurve(AnimationTokens::Curve::enter());
|
|
||||||
anim->setEndValue(target);
|
|
||||||
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
|
||||||
} else {
|
|
||||||
stopPosAnimations(card);
|
|
||||||
card->move(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionGrid::resizeEvent(QResizeEvent* event)
|
|
||||||
{
|
|
||||||
QWidget::resizeEvent(event);
|
|
||||||
relayout(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionGrid::onCardDragStart(CollectionCard* card, const QPoint& globalPos)
|
|
||||||
{
|
|
||||||
if (!card) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_dragCard = card;
|
|
||||||
m_dragOriginalIndex = m_cards.indexOf(card);
|
|
||||||
m_dragCurrentIndex = m_dragOriginalIndex;
|
|
||||||
m_lastDragGlobalPos = globalPos;
|
|
||||||
|
|
||||||
for (auto* currentCard : m_cards) {
|
|
||||||
stopPosAnimations(currentCard);
|
|
||||||
}
|
|
||||||
|
|
||||||
card->setDragging(true);
|
|
||||||
card->raise();
|
|
||||||
m_autoScrollTimer->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionGrid::onCardDragMove(CollectionCard* card, const QPoint& globalPos)
|
|
||||||
{
|
|
||||||
if (!card || card != m_dragCard) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_lastDragGlobalPos = globalPos;
|
|
||||||
updateDragPosition(card, globalPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionGrid::onCardDragEnd(CollectionCard* card)
|
|
||||||
{
|
|
||||||
if (!card) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
card->setDragging(false);
|
|
||||||
m_autoScrollTimer->stop();
|
|
||||||
|
|
||||||
CollectionCard* dragCard = m_dragCard;
|
|
||||||
const int finalIndex = qMax(0, m_dragCurrentIndex);
|
|
||||||
const bool changed = (m_dragOriginalIndex != m_dragCurrentIndex);
|
|
||||||
|
|
||||||
m_dragCard = nullptr;
|
|
||||||
m_dragOriginalIndex = -1;
|
|
||||||
m_dragCurrentIndex = -1;
|
|
||||||
|
|
||||||
stopPosAnimations(card);
|
|
||||||
const QPoint target = posForIndex(finalIndex, m_columns);
|
|
||||||
if (MotionPrefs::reduceMotion()) {
|
|
||||||
card->move(target);
|
|
||||||
} else {
|
|
||||||
auto* anim = new QPropertyAnimation(card, "pos", card);
|
|
||||||
anim->setDuration(AnimationTokens::DurationMs::kFast);
|
|
||||||
anim->setEasingCurve(AnimationTokens::Curve::enter());
|
|
||||||
anim->setEndValue(target);
|
|
||||||
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed && dragCard) {
|
|
||||||
QStringList newOrderIds;
|
|
||||||
for (auto* currentCard : m_cards) {
|
|
||||||
newOrderIds.append(currentCard->itemId());
|
|
||||||
}
|
|
||||||
emit orderChanged(newOrderIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
#ifndef COLLECTIONGRID_H
|
|
||||||
#define COLLECTIONGRID_H
|
|
||||||
|
|
||||||
#include <QList>
|
|
||||||
#include <QPoint>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QWidget>
|
|
||||||
|
|
||||||
class CollectionCard;
|
|
||||||
class QResizeEvent;
|
|
||||||
class QScrollArea;
|
|
||||||
class QTimer;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CollectionGrid : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit CollectionGrid(QWidget* parent = nullptr);
|
|
||||||
|
|
||||||
void setCards(const QList<CollectionCard*>& cards);
|
|
||||||
QList<CollectionCard*> cards() const { return m_cards; }
|
|
||||||
void relayout(bool animate = false);
|
|
||||||
|
|
||||||
|
|
||||||
static constexpr int CardWidth = 170;
|
|
||||||
static constexpr int CardHeight = 246;
|
|
||||||
static constexpr int CardSpacing = 12;
|
|
||||||
static constexpr int LeftPadding = 4;
|
|
||||||
static constexpr int RightPadding = 20;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void orderChanged(const QStringList& newOrderIds);
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
void onCardDragStart(CollectionCard* card, const QPoint& globalPos);
|
|
||||||
void onCardDragMove(CollectionCard* card, const QPoint& globalPos);
|
|
||||||
void onCardDragEnd(CollectionCard* card);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void resizeEvent(QResizeEvent* event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
int cardHeightForWidth(int width) const;
|
|
||||||
int columnsForWidth(int width) const;
|
|
||||||
int contentWidth() const;
|
|
||||||
int cardWidthForColumn(int column, int columns) const;
|
|
||||||
int columnX(int column, int columns) const;
|
|
||||||
int columnForX(int x, int columns) const;
|
|
||||||
QPoint posForIndex(int index, int columns) const;
|
|
||||||
int indexForPos(const QPoint& pos, int columns) const;
|
|
||||||
QScrollArea* parentScrollArea() const;
|
|
||||||
int autoScrollDelta(const QPoint& globalPos) const;
|
|
||||||
void updateDragPosition(CollectionCard* card, const QPoint& globalPos);
|
|
||||||
|
|
||||||
QList<CollectionCard*> m_cards;
|
|
||||||
CollectionCard* m_dragCard = nullptr;
|
|
||||||
int m_dragOriginalIndex = -1;
|
|
||||||
int m_dragCurrentIndex = -1;
|
|
||||||
int m_columns = 3;
|
|
||||||
int m_rowHeight = CardHeight;
|
|
||||||
QPoint m_lastDragGlobalPos;
|
|
||||||
QTimer* m_autoScrollTimer = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,887 +0,0 @@
|
|||||||
#include "danmakuserverdialog.h"
|
|
||||||
|
|
||||||
#include "danmakuserverlistitemwidget.h"
|
|
||||||
#include "modernmessagebox.h"
|
|
||||||
#include "services/danmaku/danmakusettings.h"
|
|
||||||
|
|
||||||
#include <QAbstractItemView>
|
|
||||||
#include <QEvent>
|
|
||||||
#include <QFormLayout>
|
|
||||||
#include <QFrame>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QLineEdit>
|
|
||||||
#include <QListWidget>
|
|
||||||
#include <QListWidgetItem>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QSignalBlocker>
|
|
||||||
#include <QShowEvent>
|
|
||||||
#include <QUrl>
|
|
||||||
#include <QUuid>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
QString fallbackServerName(const DanmakuServerDefinition &server)
|
|
||||||
{
|
|
||||||
return server.name.trimmed().isEmpty() ? server.baseUrl.trimmed()
|
|
||||||
: server.name.trimmed();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
DanmakuServerDialog::DanmakuServerDialog(QWidget *parent)
|
|
||||||
: ModernDialogBase(parent)
|
|
||||||
{
|
|
||||||
setTitle(tr("Danmaku Servers"));
|
|
||||||
setMinimumSize(920, 620);
|
|
||||||
|
|
||||||
auto *hintLabel = new QLabel(
|
|
||||||
tr("Manage multiple danmaku servers here. Click a server card to edit "
|
|
||||||
"it and choose the active server used for matching and loading "
|
|
||||||
"danmaku."),
|
|
||||||
this);
|
|
||||||
hintLabel->setObjectName("dialog-text");
|
|
||||||
hintLabel->setWordWrap(true);
|
|
||||||
contentLayout()->addWidget(hintLabel);
|
|
||||||
contentLayout()->addSpacing(8);
|
|
||||||
|
|
||||||
auto *bodyLayout = new QHBoxLayout();
|
|
||||||
bodyLayout->setSpacing(18);
|
|
||||||
|
|
||||||
auto *listPanel = new QFrame(this);
|
|
||||||
listPanel->setObjectName("DanmakuServerListPanel");
|
|
||||||
listPanel->setMinimumWidth(360);
|
|
||||||
auto *listLayout = new QVBoxLayout(listPanel);
|
|
||||||
listLayout->setContentsMargins(18, 18, 18, 18);
|
|
||||||
listLayout->setSpacing(12);
|
|
||||||
|
|
||||||
auto *listTitle = new QLabel(tr("Servers"), listPanel);
|
|
||||||
listTitle->setObjectName("SettingsCardTitle");
|
|
||||||
listLayout->addWidget(listTitle);
|
|
||||||
|
|
||||||
auto *listDesc = new QLabel(
|
|
||||||
tr("Each item shows the danmaku server name, address, app "
|
|
||||||
"credentials, and description. The built-in official "
|
|
||||||
"DandanPlay server supports anime only."),
|
|
||||||
listPanel);
|
|
||||||
listDesc->setObjectName("SettingsCardDesc");
|
|
||||||
listDesc->setWordWrap(true);
|
|
||||||
listLayout->addWidget(listDesc);
|
|
||||||
|
|
||||||
auto *actionRow = new QHBoxLayout();
|
|
||||||
actionRow->setSpacing(10);
|
|
||||||
|
|
||||||
m_addButton = new QPushButton(tr("Add"), listPanel);
|
|
||||||
m_addButton->setObjectName("SettingsCardButton");
|
|
||||||
m_addButton->setCursor(Qt::PointingHandCursor);
|
|
||||||
m_addButton->setFixedHeight(32);
|
|
||||||
actionRow->addWidget(m_addButton);
|
|
||||||
actionRow->addStretch();
|
|
||||||
listLayout->addLayout(actionRow);
|
|
||||||
|
|
||||||
m_serverList = new QListWidget(listPanel);
|
|
||||||
m_serverList->setObjectName("DanmakuServerList");
|
|
||||||
m_serverList->setFrameShape(QFrame::NoFrame);
|
|
||||||
m_serverList->setCursor(Qt::PointingHandCursor);
|
|
||||||
m_serverList->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
||||||
m_serverList->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
|
||||||
m_serverList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
||||||
m_serverList->setSpacing(10);
|
|
||||||
m_serverList->viewport()->installEventFilter(this);
|
|
||||||
listLayout->addWidget(m_serverList, 1);
|
|
||||||
|
|
||||||
bodyLayout->addWidget(listPanel, 1);
|
|
||||||
|
|
||||||
auto *editorPanel = new QFrame(this);
|
|
||||||
editorPanel->setObjectName("DanmakuServerEditorPanel");
|
|
||||||
editorPanel->setMinimumWidth(420);
|
|
||||||
auto *editorLayout = new QVBoxLayout(editorPanel);
|
|
||||||
editorLayout->setContentsMargins(18, 18, 18, 18);
|
|
||||||
editorLayout->setSpacing(14);
|
|
||||||
|
|
||||||
auto *editorTitle = new QLabel(tr("Edit Selected Server"), editorPanel);
|
|
||||||
editorTitle->setObjectName("DanmakuServerEditorTitle");
|
|
||||||
editorLayout->addWidget(editorTitle);
|
|
||||||
|
|
||||||
auto *editorDesc = new QLabel(
|
|
||||||
tr("Update the server name, address, description, and app credentials for the "
|
|
||||||
"selected danmaku server. Official DandanPlay endpoints require "
|
|
||||||
"App ID and App Secret and support anime only."),
|
|
||||||
editorPanel);
|
|
||||||
editorDesc->setObjectName("DanmakuServerEditorDesc");
|
|
||||||
editorDesc->setWordWrap(true);
|
|
||||||
editorLayout->addWidget(editorDesc);
|
|
||||||
|
|
||||||
auto *formLayout = new QFormLayout();
|
|
||||||
formLayout->setContentsMargins(0, 4, 0, 0);
|
|
||||||
formLayout->setHorizontalSpacing(14);
|
|
||||||
formLayout->setVerticalSpacing(14);
|
|
||||||
formLayout->setLabelAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
|
||||||
|
|
||||||
auto *nameLabel = new QLabel(tr("Name"), editorPanel);
|
|
||||||
nameLabel->setObjectName("LibraryMetadataFieldLabel");
|
|
||||||
m_nameEdit = new QLineEdit(editorPanel);
|
|
||||||
m_nameEdit->setObjectName("ManageLibInput");
|
|
||||||
m_nameEdit->setClearButtonEnabled(true);
|
|
||||||
m_nameEdit->setPlaceholderText(tr("e.g., Official DandanPlay"));
|
|
||||||
formLayout->addRow(nameLabel, m_nameEdit);
|
|
||||||
|
|
||||||
auto *urlLabel = new QLabel(tr("Server URL"), editorPanel);
|
|
||||||
urlLabel->setObjectName("LibraryMetadataFieldLabel");
|
|
||||||
m_baseUrlEdit = new QLineEdit(editorPanel);
|
|
||||||
m_baseUrlEdit->setObjectName("ManageLibInput");
|
|
||||||
m_baseUrlEdit->setClearButtonEnabled(true);
|
|
||||||
m_baseUrlEdit->setPlaceholderText(
|
|
||||||
QStringLiteral("https://api.dandanplay.net"));
|
|
||||||
formLayout->addRow(urlLabel, m_baseUrlEdit);
|
|
||||||
|
|
||||||
m_descriptionLabel = new QLabel(tr("Description"), editorPanel);
|
|
||||||
m_descriptionLabel->setObjectName("LibraryMetadataFieldLabel");
|
|
||||||
m_descriptionEdit = new QLineEdit(editorPanel);
|
|
||||||
m_descriptionEdit->setObjectName("ManageLibInput");
|
|
||||||
m_descriptionEdit->setClearButtonEnabled(true);
|
|
||||||
m_descriptionEdit->setPlaceholderText(tr("e.g., Supports anime only"));
|
|
||||||
formLayout->addRow(m_descriptionLabel, m_descriptionEdit);
|
|
||||||
|
|
||||||
m_appIdLabel = new QLabel(tr("App ID"), editorPanel);
|
|
||||||
m_appIdLabel->setObjectName("LibraryMetadataFieldLabel");
|
|
||||||
m_appIdEdit = new QLineEdit(editorPanel);
|
|
||||||
m_appIdEdit->setObjectName("ManageLibInput");
|
|
||||||
m_appIdEdit->setClearButtonEnabled(true);
|
|
||||||
m_appIdEdit->setPlaceholderText(tr("Required for official DandanPlay"));
|
|
||||||
formLayout->addRow(m_appIdLabel, m_appIdEdit);
|
|
||||||
|
|
||||||
m_appSecretLabel = new QLabel(tr("App Secret"), editorPanel);
|
|
||||||
m_appSecretLabel->setObjectName("LibraryMetadataFieldLabel");
|
|
||||||
m_appSecretEdit = new QLineEdit(editorPanel);
|
|
||||||
m_appSecretEdit->setObjectName("ManageLibInput");
|
|
||||||
m_appSecretEdit->setClearButtonEnabled(true);
|
|
||||||
m_appSecretEdit->setEchoMode(QLineEdit::Password);
|
|
||||||
m_appSecretEdit->setPlaceholderText(
|
|
||||||
tr("Required for official DandanPlay"));
|
|
||||||
formLayout->addRow(m_appSecretLabel, m_appSecretEdit);
|
|
||||||
|
|
||||||
editorLayout->addLayout(formLayout);
|
|
||||||
editorLayout->addStretch();
|
|
||||||
bodyLayout->addWidget(editorPanel, 1);
|
|
||||||
|
|
||||||
contentLayout()->addLayout(bodyLayout);
|
|
||||||
contentLayout()->addSpacing(24);
|
|
||||||
|
|
||||||
auto *buttonLayout = new QHBoxLayout();
|
|
||||||
buttonLayout->setSpacing(12);
|
|
||||||
buttonLayout->addStretch();
|
|
||||||
|
|
||||||
auto *cancelButton = new QPushButton(tr("Cancel"), this);
|
|
||||||
cancelButton->setObjectName("dialog-btn-cancel");
|
|
||||||
cancelButton->setCursor(Qt::PointingHandCursor);
|
|
||||||
connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject);
|
|
||||||
buttonLayout->addWidget(cancelButton);
|
|
||||||
|
|
||||||
auto *saveButton = new QPushButton(tr("Save"), this);
|
|
||||||
saveButton->setObjectName("dialog-btn-primary");
|
|
||||||
saveButton->setCursor(Qt::PointingHandCursor);
|
|
||||||
connect(saveButton, &QPushButton::clicked, this,
|
|
||||||
[this]() { accept(); });
|
|
||||||
buttonLayout->addWidget(saveButton);
|
|
||||||
|
|
||||||
contentLayout()->addLayout(buttonLayout);
|
|
||||||
|
|
||||||
connect(m_serverList, &QListWidget::currentRowChanged, this,
|
|
||||||
[this](int row) {
|
|
||||||
if (m_loading || row < 0 || row >= m_servers.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_editingServerId = m_servers.at(row).id;
|
|
||||||
if (m_servers.at(row).enabled) {
|
|
||||||
m_selectedServerId = m_servers.at(row).id;
|
|
||||||
}
|
|
||||||
loadCurrentServer();
|
|
||||||
updateSelectionState();
|
|
||||||
updateEditorState();
|
|
||||||
updateActionState();
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(m_nameEdit, &QLineEdit::textChanged, this,
|
|
||||||
[this](const QString &text) { updateCurrentServerName(text); });
|
|
||||||
connect(m_baseUrlEdit, &QLineEdit::textChanged, this,
|
|
||||||
[this](const QString &text) { updateCurrentServerUrl(text); });
|
|
||||||
connect(m_descriptionEdit, &QLineEdit::textChanged, this,
|
|
||||||
[this](const QString &text) {
|
|
||||||
updateCurrentServerDescription(text);
|
|
||||||
});
|
|
||||||
connect(m_appIdEdit, &QLineEdit::textChanged, this,
|
|
||||||
[this](const QString &text) { updateCurrentServerAppId(text); });
|
|
||||||
connect(m_appSecretEdit, &QLineEdit::textChanged, this,
|
|
||||||
[this](const QString &text) {
|
|
||||||
updateCurrentServerAppSecret(text);
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(m_addButton, &QPushButton::clicked, this, [this]() {
|
|
||||||
DanmakuServerDefinition server =
|
|
||||||
DanmakuSettings::builtInOfficialDandanplayServer();
|
|
||||||
server.id = createServerId();
|
|
||||||
server.builtIn = false;
|
|
||||||
server.contentScope.clear();
|
|
||||||
server.description.clear();
|
|
||||||
server.appId.clear();
|
|
||||||
server.appSecret.clear();
|
|
||||||
server.name = tr("Danmaku Server %1").arg(m_servers.size() + 1);
|
|
||||||
server.enabled = true;
|
|
||||||
m_servers.append(server);
|
|
||||||
refreshServerList();
|
|
||||||
selectServer(m_servers.size() - 1);
|
|
||||||
m_nameEdit->setFocus();
|
|
||||||
m_nameEdit->selectAll();
|
|
||||||
});
|
|
||||||
|
|
||||||
setServers({DanmakuSettings::builtInOfficialDandanplayServer()},
|
|
||||||
QStringLiteral("default"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerDialog::setServers(QList<DanmakuServerDefinition> servers,
|
|
||||||
QString selectedServerId)
|
|
||||||
{
|
|
||||||
if (servers.isEmpty()) {
|
|
||||||
servers.append(DanmakuSettings::builtInOfficialDandanplayServer());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (DanmakuServerDefinition &server : servers) {
|
|
||||||
server.id = server.id.trimmed();
|
|
||||||
server.name = server.name.trimmed();
|
|
||||||
server.provider = server.provider.trimmed();
|
|
||||||
server.baseUrl = normalizeBaseUrl(server.baseUrl);
|
|
||||||
server.description = server.description.trimmed();
|
|
||||||
server.appId = server.appId.trimmed();
|
|
||||||
server.appSecret = server.appSecret.trimmed();
|
|
||||||
server.contentScope = server.contentScope.trimmed().toLower();
|
|
||||||
|
|
||||||
if (server.id.isEmpty()) {
|
|
||||||
server.id = createServerId();
|
|
||||||
}
|
|
||||||
if (server.builtIn) {
|
|
||||||
const DanmakuServerDefinition builtIn =
|
|
||||||
DanmakuSettings::builtInOfficialDandanplayServer();
|
|
||||||
server.id = builtIn.id;
|
|
||||||
server.provider = builtIn.provider;
|
|
||||||
server.baseUrl = builtIn.baseUrl;
|
|
||||||
server.appId = builtIn.appId;
|
|
||||||
server.appSecret = builtIn.appSecret;
|
|
||||||
server.description.clear();
|
|
||||||
server.contentScope = builtIn.contentScope;
|
|
||||||
if (server.name.isEmpty()) {
|
|
||||||
server.name = builtIn.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (server.provider.isEmpty()) {
|
|
||||||
server.provider = QStringLiteral("dandanplay");
|
|
||||||
}
|
|
||||||
if (server.name.isEmpty()) {
|
|
||||||
server.name = fallbackServerName(server);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_servers = std::move(servers);
|
|
||||||
m_selectedServerId = selectedServerId.trimmed();
|
|
||||||
if (m_selectedServerId.trimmed().isEmpty() && !m_servers.isEmpty()) {
|
|
||||||
const int activeIndex = preferredServerIndex(0);
|
|
||||||
if (activeIndex >= 0) {
|
|
||||||
m_selectedServerId = m_servers.at(activeIndex).id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_editingServerId = m_selectedServerId;
|
|
||||||
refreshServerList();
|
|
||||||
selectServer(editingServerIndex());
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<DanmakuServerDefinition> DanmakuServerDialog::servers() const
|
|
||||||
{
|
|
||||||
return m_servers;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString DanmakuServerDialog::selectedServerId() const
|
|
||||||
{
|
|
||||||
if (enabledServerCount() <= 0) {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const int index = preferredServerIndex(currentServerIndex());
|
|
||||||
if (index >= 0 && index < m_servers.size()) {
|
|
||||||
return m_servers.at(index).id;
|
|
||||||
}
|
|
||||||
return m_selectedServerId.trimmed().isEmpty() && !m_servers.isEmpty()
|
|
||||||
? m_servers.first().id
|
|
||||||
: m_selectedServerId;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerDialog::accept()
|
|
||||||
{
|
|
||||||
if (m_servers.isEmpty()) {
|
|
||||||
ModernMessageBox::warning(
|
|
||||||
this, tr("Danmaku Servers"),
|
|
||||||
tr("At least one danmaku server is required."), tr("OK"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const DanmakuServerDefinition &server : std::as_const(m_servers)) {
|
|
||||||
if (server.name.trimmed().isEmpty()) {
|
|
||||||
m_editingServerId = server.id;
|
|
||||||
if (server.enabled) {
|
|
||||||
m_selectedServerId = server.id;
|
|
||||||
}
|
|
||||||
refreshServerList();
|
|
||||||
selectServer(editingServerIndex());
|
|
||||||
m_nameEdit->setFocus();
|
|
||||||
ModernMessageBox::warning(
|
|
||||||
this, tr("Danmaku Servers"),
|
|
||||||
tr("Every danmaku server must have a name."), tr("OK"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (server.baseUrl.trimmed().isEmpty()) {
|
|
||||||
m_editingServerId = server.id;
|
|
||||||
if (server.enabled) {
|
|
||||||
m_selectedServerId = server.id;
|
|
||||||
}
|
|
||||||
refreshServerList();
|
|
||||||
selectServer(editingServerIndex());
|
|
||||||
m_baseUrlEdit->setFocus();
|
|
||||||
ModernMessageBox::warning(
|
|
||||||
this, tr("Danmaku Servers"),
|
|
||||||
tr("Every danmaku server must have a server URL."), tr("OK"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QUrl url = QUrl::fromUserInput(server.baseUrl.trimmed());
|
|
||||||
const QString scheme = url.scheme().trimmed().toLower();
|
|
||||||
if (!url.isValid() || url.host().trimmed().isEmpty() ||
|
|
||||||
(scheme != QStringLiteral("http") &&
|
|
||||||
scheme != QStringLiteral("https"))) {
|
|
||||||
m_editingServerId = server.id;
|
|
||||||
if (server.enabled) {
|
|
||||||
m_selectedServerId = server.id;
|
|
||||||
}
|
|
||||||
refreshServerList();
|
|
||||||
selectServer(editingServerIndex());
|
|
||||||
m_baseUrlEdit->setFocus();
|
|
||||||
ModernMessageBox::warning(
|
|
||||||
this, tr("Danmaku Servers"),
|
|
||||||
tr("Server URL must be a valid http or https address."),
|
|
||||||
tr("OK"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString activeServerId = selectedServerId();
|
|
||||||
if (!activeServerId.trimmed().isEmpty()) {
|
|
||||||
m_selectedServerId = activeServerId;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDialog::accept();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DanmakuServerDialog::eventFilter(QObject *watched, QEvent *event)
|
|
||||||
{
|
|
||||||
if (m_serverList && watched == m_serverList->viewport() &&
|
|
||||||
event && event->type() == QEvent::Resize) {
|
|
||||||
syncAllServerListItemSizes();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ModernDialogBase::eventFilter(watched, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerDialog::showEvent(QShowEvent *event)
|
|
||||||
{
|
|
||||||
ModernDialogBase::showEvent(event);
|
|
||||||
syncAllServerListItemSizes();
|
|
||||||
}
|
|
||||||
|
|
||||||
int DanmakuServerDialog::currentServerIndex() const
|
|
||||||
{
|
|
||||||
for (int index = 0; index < m_servers.size(); ++index) {
|
|
||||||
if (m_servers.at(index).id == m_selectedServerId) {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m_servers.isEmpty() ? -1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int DanmakuServerDialog::editingServerIndex() const
|
|
||||||
{
|
|
||||||
for (int index = 0; index < m_servers.size(); ++index) {
|
|
||||||
if (m_servers.at(index).id == m_editingServerId) {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m_servers.isEmpty() ? -1 : currentServerIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
int DanmakuServerDialog::enabledServerCount() const
|
|
||||||
{
|
|
||||||
int count = 0;
|
|
||||||
for (const DanmakuServerDefinition &server : m_servers) {
|
|
||||||
if (server.enabled) {
|
|
||||||
++count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
int DanmakuServerDialog::preferredServerIndex(int preferredIndex) const
|
|
||||||
{
|
|
||||||
if (m_servers.isEmpty()) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int normalizedPreferred =
|
|
||||||
qBound(0, preferredIndex < 0 ? 0 : preferredIndex, m_servers.size() - 1);
|
|
||||||
|
|
||||||
if (enabledServerCount() > 0) {
|
|
||||||
for (int offset = 0; offset < m_servers.size(); ++offset) {
|
|
||||||
const int forwardIndex = normalizedPreferred + offset;
|
|
||||||
if (forwardIndex < m_servers.size() &&
|
|
||||||
m_servers.at(forwardIndex).enabled) {
|
|
||||||
return forwardIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int backwardIndex = normalizedPreferred - offset;
|
|
||||||
if (offset > 0 && backwardIndex >= 0 &&
|
|
||||||
m_servers.at(backwardIndex).enabled) {
|
|
||||||
return backwardIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizedPreferred;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerDialog::removeServer(int index)
|
|
||||||
{
|
|
||||||
if (index < 0 || index >= m_servers.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_servers.at(index).builtIn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_servers.size() <= 1) {
|
|
||||||
ModernMessageBox::warning(
|
|
||||||
this, tr("Danmaku Servers"),
|
|
||||||
tr("At least one danmaku server is required."), tr("OK"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString removedServerId = m_servers.at(index).id;
|
|
||||||
const QString serverName = fallbackServerName(m_servers.at(index));
|
|
||||||
if (!ModernMessageBox::question(
|
|
||||||
this, tr("Remove Danmaku Server"),
|
|
||||||
tr("Remove \"%1\" from the danmaku server list?").arg(serverName),
|
|
||||||
tr("Remove"), tr("Cancel"), ModernMessageBox::Danger,
|
|
||||||
ModernMessageBox::Warning)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool removingSelected = removedServerId == m_selectedServerId;
|
|
||||||
const bool removingEditing = removedServerId == m_editingServerId;
|
|
||||||
m_servers.removeAt(index);
|
|
||||||
|
|
||||||
if (m_servers.isEmpty()) {
|
|
||||||
refreshServerList();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removingSelected) {
|
|
||||||
const int nextActiveIndex =
|
|
||||||
preferredServerIndex(qBound(0, index, m_servers.size() - 1));
|
|
||||||
if (nextActiveIndex >= 0 && nextActiveIndex < m_servers.size()) {
|
|
||||||
m_selectedServerId = m_servers.at(nextActiveIndex).id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removingEditing) {
|
|
||||||
const int nextEditingIndex =
|
|
||||||
qBound(0, index, m_servers.size() - 1);
|
|
||||||
m_editingServerId = m_servers.at(nextEditingIndex).id;
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshServerList();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerDialog::selectServer(int index)
|
|
||||||
{
|
|
||||||
if (index < 0 || index >= m_servers.size()) {
|
|
||||||
updateEditorState();
|
|
||||||
updateActionState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_loading = true;
|
|
||||||
{
|
|
||||||
const QSignalBlocker blocker(m_serverList);
|
|
||||||
m_serverList->setCurrentRow(index);
|
|
||||||
}
|
|
||||||
m_editingServerId = m_servers.at(index).id;
|
|
||||||
if (m_servers.at(index).enabled) {
|
|
||||||
m_selectedServerId = m_servers.at(index).id;
|
|
||||||
}
|
|
||||||
m_loading = false;
|
|
||||||
|
|
||||||
loadCurrentServer();
|
|
||||||
updateSelectionState();
|
|
||||||
updateEditorState();
|
|
||||||
updateActionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerDialog::refreshServerList()
|
|
||||||
{
|
|
||||||
const int activeIndex = preferredServerIndex(currentServerIndex());
|
|
||||||
const int editorIndex = editingServerIndex();
|
|
||||||
|
|
||||||
m_loading = true;
|
|
||||||
m_serverList->blockSignals(true);
|
|
||||||
m_serverList->clear();
|
|
||||||
|
|
||||||
for (int index = 0; index < m_servers.size(); ++index) {
|
|
||||||
const DanmakuServerDefinition &server = m_servers.at(index);
|
|
||||||
auto *item = new QListWidgetItem();
|
|
||||||
auto *widget = new DanmakuServerListItemWidget(m_serverList);
|
|
||||||
widget->setServer(server);
|
|
||||||
widget->setRemovable(m_servers.size() > 1 && !server.builtIn);
|
|
||||||
item->setData(Qt::UserRole, server.id);
|
|
||||||
m_serverList->addItem(item);
|
|
||||||
m_serverList->setItemWidget(item, widget);
|
|
||||||
|
|
||||||
connect(widget, &DanmakuServerListItemWidget::clicked, this,
|
|
||||||
[this, item]() {
|
|
||||||
if (!item) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int row = m_serverList->row(item);
|
|
||||||
if (row < 0 || row >= m_servers.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
selectServer(row);
|
|
||||||
});
|
|
||||||
connect(widget, &DanmakuServerListItemWidget::enabledChanged, this,
|
|
||||||
[this, item](bool enabled) {
|
|
||||||
if (!item) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int row = m_serverList->row(item);
|
|
||||||
if (row < 0 || row >= m_servers.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
updateServerEnabled(row, enabled);
|
|
||||||
});
|
|
||||||
connect(widget, &DanmakuServerListItemWidget::removeRequested, this,
|
|
||||||
[this, item]() {
|
|
||||||
if (!item) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeServer(m_serverList->row(item));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_servers.isEmpty()) {
|
|
||||||
const int row = qBound(
|
|
||||||
0, editorIndex < 0 ? activeIndex : editorIndex, m_servers.size() - 1);
|
|
||||||
m_serverList->setCurrentRow(row);
|
|
||||||
m_editingServerId = m_servers.at(row).id;
|
|
||||||
|
|
||||||
const int normalizedActiveIndex =
|
|
||||||
activeIndex >= 0 ? activeIndex : preferredServerIndex(row);
|
|
||||||
if (normalizedActiveIndex >= 0) {
|
|
||||||
m_selectedServerId = m_servers.at(normalizedActiveIndex).id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_serverList->blockSignals(false);
|
|
||||||
m_loading = false;
|
|
||||||
syncAllServerListItemSizes();
|
|
||||||
loadCurrentServer();
|
|
||||||
updateSelectionState();
|
|
||||||
updateEditorState();
|
|
||||||
updateActionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerDialog::refreshServerListItem(int index)
|
|
||||||
{
|
|
||||||
if (index < 0 || index >= m_servers.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto *item = m_serverList->item(index);
|
|
||||||
if (!item) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto *widget =
|
|
||||||
qobject_cast<DanmakuServerListItemWidget *>(
|
|
||||||
m_serverList->itemWidget(item));
|
|
||||||
if (!widget) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
widget->setServer(m_servers.at(index));
|
|
||||||
widget->setRemovable(m_servers.size() > 1 &&
|
|
||||||
!m_servers.at(index).builtIn);
|
|
||||||
syncServerListItemSize(index);
|
|
||||||
updateSelectionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerDialog::syncServerListItemSize(int index)
|
|
||||||
{
|
|
||||||
constexpr int kListItemRightInset = 8;
|
|
||||||
|
|
||||||
if (!m_serverList || index < 0 || index >= m_serverList->count() ||
|
|
||||||
index >= m_servers.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto *item = m_serverList->item(index);
|
|
||||||
if (!item) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto *widget = qobject_cast<DanmakuServerListItemWidget *>(
|
|
||||||
m_serverList->itemWidget(item));
|
|
||||||
if (!widget || !m_serverList->viewport()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int viewportWidth = m_serverList->viewport()->width();
|
|
||||||
const int targetWidth = viewportWidth - kListItemRightInset;
|
|
||||||
if (targetWidth <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
widget->updateGeometry();
|
|
||||||
|
|
||||||
int targetHeight = widget->hasHeightForWidth()
|
|
||||||
? widget->heightForWidth(targetWidth)
|
|
||||||
: widget->sizeHint().height();
|
|
||||||
if (targetHeight <= 0) {
|
|
||||||
widget->adjustSize();
|
|
||||||
targetHeight = widget->sizeHint().height();
|
|
||||||
}
|
|
||||||
|
|
||||||
targetHeight = qMax(targetHeight, widget->minimumSizeHint().height());
|
|
||||||
item->setSizeHint(QSize(targetWidth, targetHeight));
|
|
||||||
widget->resize(targetWidth, targetHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerDialog::syncAllServerListItemSizes()
|
|
||||||
{
|
|
||||||
for (int index = 0; index < m_serverList->count(); ++index) {
|
|
||||||
syncServerListItemSize(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerDialog::loadCurrentServer()
|
|
||||||
{
|
|
||||||
const int index = editingServerIndex();
|
|
||||||
if (index < 0 || index >= m_servers.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_loading = true;
|
|
||||||
const DanmakuServerDefinition &server = m_servers.at(index);
|
|
||||||
m_nameEdit->setText(server.name);
|
|
||||||
m_baseUrlEdit->setText(server.baseUrl);
|
|
||||||
m_descriptionEdit->setText(server.description);
|
|
||||||
m_appIdEdit->setText(server.appId);
|
|
||||||
m_appSecretEdit->setText(server.appSecret);
|
|
||||||
m_loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerDialog::updateSelectionState()
|
|
||||||
{
|
|
||||||
const int currentRow = m_serverList->currentRow();
|
|
||||||
const QString activeServerId = selectedServerId();
|
|
||||||
for (int row = 0; row < m_serverList->count(); ++row) {
|
|
||||||
auto *widget = qobject_cast<DanmakuServerListItemWidget *>(
|
|
||||||
m_serverList->itemWidget(m_serverList->item(row)));
|
|
||||||
if (widget) {
|
|
||||||
widget->setSelected(row == currentRow);
|
|
||||||
widget->setCurrentServer(m_servers.at(row).id == activeServerId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerDialog::updateEditorState()
|
|
||||||
{
|
|
||||||
const int index = editingServerIndex();
|
|
||||||
const bool hasCurrentServer = index >= 0 && index < m_servers.size();
|
|
||||||
const bool isBuiltIn = hasCurrentServer && m_servers.at(index).builtIn;
|
|
||||||
m_nameEdit->setEnabled(hasCurrentServer && !isBuiltIn);
|
|
||||||
m_baseUrlEdit->setEnabled(hasCurrentServer && !isBuiltIn);
|
|
||||||
m_descriptionLabel->setVisible(hasCurrentServer && !isBuiltIn);
|
|
||||||
m_descriptionEdit->setVisible(hasCurrentServer && !isBuiltIn);
|
|
||||||
m_descriptionEdit->setEnabled(hasCurrentServer && !isBuiltIn);
|
|
||||||
m_appIdLabel->setVisible(hasCurrentServer && !isBuiltIn);
|
|
||||||
m_appIdEdit->setVisible(hasCurrentServer && !isBuiltIn);
|
|
||||||
m_appIdEdit->setEnabled(hasCurrentServer && !isBuiltIn);
|
|
||||||
m_appSecretLabel->setVisible(hasCurrentServer && !isBuiltIn);
|
|
||||||
m_appSecretEdit->setVisible(hasCurrentServer && !isBuiltIn);
|
|
||||||
m_appSecretEdit->setEnabled(hasCurrentServer && !isBuiltIn);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerDialog::updateServerEnabled(int index, bool enabled)
|
|
||||||
{
|
|
||||||
if (index < 0 || index >= m_servers.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString previousActiveServerId = selectedServerId();
|
|
||||||
m_servers[index].enabled = enabled;
|
|
||||||
|
|
||||||
if (enabled && m_servers.at(index).id == m_editingServerId) {
|
|
||||||
m_selectedServerId = m_servers.at(index).id;
|
|
||||||
} else if (!enabled && m_servers.at(index).id == m_selectedServerId) {
|
|
||||||
const int nextIndex = preferredServerIndex(index);
|
|
||||||
if (nextIndex >= 0 && nextIndex != index) {
|
|
||||||
m_selectedServerId = m_servers.at(nextIndex).id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshServerListItem(index);
|
|
||||||
const QString activeServerId = selectedServerId();
|
|
||||||
if (activeServerId != previousActiveServerId) {
|
|
||||||
for (int row = 0; row < m_servers.size(); ++row) {
|
|
||||||
if (m_servers.at(row).id == previousActiveServerId ||
|
|
||||||
m_servers.at(row).id == activeServerId) {
|
|
||||||
refreshServerListItem(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSelectionState();
|
|
||||||
updateEditorState();
|
|
||||||
updateActionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerDialog::updateCurrentServerName(const QString &name)
|
|
||||||
{
|
|
||||||
if (m_loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int index = editingServerIndex();
|
|
||||||
if (index < 0 || index >= m_servers.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (m_servers.at(index).builtIn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_servers[index].name = name.trimmed();
|
|
||||||
refreshServerListItem(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerDialog::updateCurrentServerUrl(const QString &baseUrl)
|
|
||||||
{
|
|
||||||
if (m_loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int index = editingServerIndex();
|
|
||||||
if (index < 0 || index >= m_servers.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (m_servers.at(index).builtIn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_servers[index].baseUrl = baseUrl.trimmed();
|
|
||||||
refreshServerListItem(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerDialog::updateCurrentServerDescription(
|
|
||||||
const QString &description)
|
|
||||||
{
|
|
||||||
if (m_loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int index = editingServerIndex();
|
|
||||||
if (index < 0 || index >= m_servers.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (m_servers.at(index).builtIn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_servers[index].description = description.trimmed();
|
|
||||||
refreshServerListItem(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerDialog::updateCurrentServerAppId(const QString &appId)
|
|
||||||
{
|
|
||||||
if (m_loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int index = editingServerIndex();
|
|
||||||
if (index < 0 || index >= m_servers.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (m_servers.at(index).builtIn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_servers[index].appId = appId.trimmed();
|
|
||||||
refreshServerListItem(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerDialog::updateCurrentServerAppSecret(
|
|
||||||
const QString &appSecret)
|
|
||||||
{
|
|
||||||
if (m_loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int index = editingServerIndex();
|
|
||||||
if (index < 0 || index >= m_servers.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (m_servers.at(index).builtIn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_servers[index].appSecret = appSecret.trimmed();
|
|
||||||
refreshServerListItem(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerDialog::updateActionState()
|
|
||||||
{
|
|
||||||
for (int row = 0; row < m_serverList->count(); ++row) {
|
|
||||||
auto *widget = qobject_cast<DanmakuServerListItemWidget *>(
|
|
||||||
m_serverList->itemWidget(m_serverList->item(row)));
|
|
||||||
if (widget) {
|
|
||||||
const bool removable =
|
|
||||||
row < m_servers.size() && m_servers.size() > 1 &&
|
|
||||||
!m_servers.at(row).builtIn;
|
|
||||||
widget->setRemovable(removable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QString DanmakuServerDialog::createServerId()
|
|
||||||
{
|
|
||||||
return QUuid::createUuid().toString(QUuid::WithoutBraces);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString DanmakuServerDialog::normalizeBaseUrl(QString baseUrl)
|
|
||||||
{
|
|
||||||
baseUrl = baseUrl.trimmed();
|
|
||||||
while (baseUrl.endsWith('/') &&
|
|
||||||
!baseUrl.endsWith(QStringLiteral("://"))) {
|
|
||||||
baseUrl.chop(1);
|
|
||||||
}
|
|
||||||
return baseUrl;
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
#ifndef DANMAKUSERVERDIALOG_H
|
|
||||||
#define DANMAKUSERVERDIALOG_H
|
|
||||||
|
|
||||||
#include "moderndialogbase.h"
|
|
||||||
|
|
||||||
#include "models/danmaku/danmakumodels.h"
|
|
||||||
|
|
||||||
#include <QList>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
class QListWidget;
|
|
||||||
class QLineEdit;
|
|
||||||
class QLabel;
|
|
||||||
class QPushButton;
|
|
||||||
class QEvent;
|
|
||||||
class QShowEvent;
|
|
||||||
|
|
||||||
class DanmakuServerDialog : public ModernDialogBase
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit DanmakuServerDialog(QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
void setServers(QList<DanmakuServerDefinition> servers,
|
|
||||||
QString selectedServerId = QString());
|
|
||||||
QList<DanmakuServerDefinition> servers() const;
|
|
||||||
QString selectedServerId() const;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void accept() override;
|
|
||||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
|
||||||
void showEvent(QShowEvent *event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
int currentServerIndex() const;
|
|
||||||
int editingServerIndex() const;
|
|
||||||
int enabledServerCount() const;
|
|
||||||
int preferredServerIndex(int preferredIndex) const;
|
|
||||||
void removeServer(int index);
|
|
||||||
void selectServer(int index);
|
|
||||||
void refreshServerList();
|
|
||||||
void refreshServerListItem(int index);
|
|
||||||
void syncServerListItemSize(int index);
|
|
||||||
void syncAllServerListItemSizes();
|
|
||||||
void loadCurrentServer();
|
|
||||||
void updateSelectionState();
|
|
||||||
void updateEditorState();
|
|
||||||
void updateServerEnabled(int index, bool enabled);
|
|
||||||
void updateCurrentServerName(const QString &name);
|
|
||||||
void updateCurrentServerUrl(const QString &baseUrl);
|
|
||||||
void updateCurrentServerDescription(const QString &description);
|
|
||||||
void updateCurrentServerAppId(const QString &appId);
|
|
||||||
void updateCurrentServerAppSecret(const QString &appSecret);
|
|
||||||
void updateActionState();
|
|
||||||
|
|
||||||
static QString createServerId();
|
|
||||||
static QString normalizeBaseUrl(QString baseUrl);
|
|
||||||
|
|
||||||
QList<DanmakuServerDefinition> m_servers;
|
|
||||||
QString m_selectedServerId;
|
|
||||||
QString m_editingServerId;
|
|
||||||
bool m_loading = false;
|
|
||||||
|
|
||||||
QListWidget *m_serverList = nullptr;
|
|
||||||
QLineEdit *m_nameEdit = nullptr;
|
|
||||||
QLineEdit *m_baseUrlEdit = nullptr;
|
|
||||||
QLabel *m_descriptionLabel = nullptr;
|
|
||||||
QLineEdit *m_descriptionEdit = nullptr;
|
|
||||||
QLabel *m_appIdLabel = nullptr;
|
|
||||||
QLineEdit *m_appIdEdit = nullptr;
|
|
||||||
QLabel *m_appSecretLabel = nullptr;
|
|
||||||
QLineEdit *m_appSecretEdit = nullptr;
|
|
||||||
QPushButton *m_addButton = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
#include "danmakuserverlistitemwidget.h"
|
|
||||||
|
|
||||||
#include "../managers/thememanager.h"
|
|
||||||
#include "modernswitch.h"
|
|
||||||
|
|
||||||
#include <QEvent>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QSignalBlocker>
|
|
||||||
#include <QSizePolicy>
|
|
||||||
#include <QStyle>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
QColor activeRemoveIconColor()
|
|
||||||
{
|
|
||||||
return ThemeManager::instance()->isDarkMode()
|
|
||||||
? QColor(QStringLiteral("#FCA5A5"))
|
|
||||||
: QColor(QStringLiteral("#DC2626"));
|
|
||||||
}
|
|
||||||
|
|
||||||
QColor inactiveRemoveIconColor()
|
|
||||||
{
|
|
||||||
return ThemeManager::instance()->isDarkMode()
|
|
||||||
? QColor(QStringLiteral("#64748B"))
|
|
||||||
: QColor(QStringLiteral("#94A3B8"));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
DanmakuServerListItemWidget::DanmakuServerListItemWidget(QWidget *parent)
|
|
||||||
: QFrame(parent)
|
|
||||||
{
|
|
||||||
constexpr int actionColumnWidth = 74;
|
|
||||||
|
|
||||||
setObjectName("DanmakuServerListRow");
|
|
||||||
setCursor(Qt::PointingHandCursor);
|
|
||||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
|
||||||
|
|
||||||
auto *outerLayout = new QHBoxLayout(this);
|
|
||||||
outerLayout->setContentsMargins(0, 0, 8, 0);
|
|
||||||
outerLayout->setSpacing(0);
|
|
||||||
|
|
||||||
m_cardFrame = new QFrame(this);
|
|
||||||
m_cardFrame->setObjectName("DanmakuServerCard");
|
|
||||||
m_cardFrame->setProperty("selected", false);
|
|
||||||
m_cardFrame->setProperty("currentServer", false);
|
|
||||||
m_cardFrame->setProperty("serverEnabled", true);
|
|
||||||
outerLayout->addWidget(m_cardFrame);
|
|
||||||
|
|
||||||
auto *layout = new QHBoxLayout(m_cardFrame);
|
|
||||||
layout->setContentsMargins(16, 14, 16, 14);
|
|
||||||
layout->setSpacing(12);
|
|
||||||
|
|
||||||
auto *textLayout = new QVBoxLayout();
|
|
||||||
textLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
textLayout->setSpacing(4);
|
|
||||||
layout->addLayout(textLayout, 1);
|
|
||||||
|
|
||||||
auto *actionContainer = new QWidget(m_cardFrame);
|
|
||||||
actionContainer->setFixedWidth(actionColumnWidth);
|
|
||||||
actionContainer->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
|
|
||||||
|
|
||||||
auto *actionLayout = new QHBoxLayout(actionContainer);
|
|
||||||
actionLayout->setContentsMargins(0, 0, 2, 0);
|
|
||||||
actionLayout->setSpacing(4);
|
|
||||||
actionLayout->addStretch(1);
|
|
||||||
layout->addWidget(actionContainer, 0, Qt::AlignVCenter);
|
|
||||||
|
|
||||||
m_removeButton = new QPushButton(actionContainer);
|
|
||||||
m_removeButton->setObjectName("DanmakuServerRemoveButton");
|
|
||||||
m_removeButton->setCursor(Qt::PointingHandCursor);
|
|
||||||
m_removeButton->setToolTip(tr("Remove Server"));
|
|
||||||
m_removeButton->setFixedSize(20, 20);
|
|
||||||
m_removeButton->setIconSize(QSize(10, 10));
|
|
||||||
m_removeButton->setFocusPolicy(Qt::NoFocus);
|
|
||||||
m_enabledSwitch = new ModernSwitch(actionContainer);
|
|
||||||
m_enabledSwitch->setFixedSize(m_enabledSwitch->sizeHint());
|
|
||||||
m_enabledSwitch->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
||||||
actionLayout->addWidget(m_removeButton, 0, Qt::AlignVCenter | Qt::AlignRight);
|
|
||||||
actionLayout->addWidget(m_enabledSwitch, 0, Qt::AlignVCenter | Qt::AlignRight);
|
|
||||||
|
|
||||||
m_nameLabel = new QLabel(m_cardFrame);
|
|
||||||
m_nameLabel->setObjectName("DanmakuServerCardTitle");
|
|
||||||
m_nameLabel->setWordWrap(true);
|
|
||||||
m_nameLabel->installEventFilter(this);
|
|
||||||
textLayout->addWidget(m_nameLabel);
|
|
||||||
|
|
||||||
m_baseUrlLabel = new QLabel(m_cardFrame);
|
|
||||||
m_baseUrlLabel->setObjectName("DanmakuServerCardLine");
|
|
||||||
m_baseUrlLabel->setWordWrap(true);
|
|
||||||
m_baseUrlLabel->installEventFilter(this);
|
|
||||||
textLayout->addWidget(m_baseUrlLabel);
|
|
||||||
|
|
||||||
m_credentialsLabel = new QLabel(m_cardFrame);
|
|
||||||
m_credentialsLabel->setObjectName("DanmakuServerCardMeta");
|
|
||||||
m_credentialsLabel->setWordWrap(true);
|
|
||||||
m_credentialsLabel->installEventFilter(this);
|
|
||||||
textLayout->addWidget(m_credentialsLabel);
|
|
||||||
|
|
||||||
m_cardFrame->installEventFilter(this);
|
|
||||||
refreshRemoveIcon();
|
|
||||||
|
|
||||||
connect(m_enabledSwitch, &ModernSwitch::toggled, this,
|
|
||||||
&DanmakuServerListItemWidget::enabledChanged);
|
|
||||||
connect(m_removeButton, &QPushButton::clicked, this,
|
|
||||||
&DanmakuServerListItemWidget::removeRequested);
|
|
||||||
connect(ThemeManager::instance(), &ThemeManager::themeChanged, this,
|
|
||||||
[this](ThemeManager::Theme) { refreshRemoveIcon(); });
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerListItemWidget::setServer(
|
|
||||||
const DanmakuServerDefinition &server)
|
|
||||||
{
|
|
||||||
m_server = server;
|
|
||||||
|
|
||||||
const QString displayName = server.name.trimmed().isEmpty()
|
|
||||||
? server.baseUrl.trimmed()
|
|
||||||
: server.name.trimmed();
|
|
||||||
{
|
|
||||||
const QSignalBlocker blocker(m_enabledSwitch);
|
|
||||||
m_enabledSwitch->setChecked(server.enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_cardFrame->setProperty("serverEnabled", server.enabled);
|
|
||||||
m_nameLabel->setText(displayName);
|
|
||||||
m_baseUrlLabel->setText(server.baseUrl.trimmed());
|
|
||||||
refreshCredentialsLabel();
|
|
||||||
refreshSelectionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerListItemWidget::setSelected(bool selected)
|
|
||||||
{
|
|
||||||
if (m_cardFrame->property("selected").toBool() == selected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_cardFrame->setProperty("selected", selected);
|
|
||||||
refreshSelectionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerListItemWidget::setCurrentServer(bool currentServer)
|
|
||||||
{
|
|
||||||
if (m_cardFrame->property("currentServer").toBool() == currentServer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_cardFrame->setProperty("currentServer", currentServer);
|
|
||||||
refreshSelectionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerListItemWidget::setRemovable(bool removable)
|
|
||||||
{
|
|
||||||
m_removable = removable;
|
|
||||||
m_removeButton->setVisible(removable);
|
|
||||||
m_removeButton->setEnabled(removable);
|
|
||||||
m_removeButton->setProperty("removable", removable);
|
|
||||||
m_removeButton->setToolTip(removable ? tr("Remove Server")
|
|
||||||
: QString());
|
|
||||||
m_removeButton->style()->unpolish(m_removeButton);
|
|
||||||
m_removeButton->style()->polish(m_removeButton);
|
|
||||||
refreshRemoveIcon();
|
|
||||||
updateGeometry();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DanmakuServerListItemWidget::eventFilter(QObject *watched, QEvent *event)
|
|
||||||
{
|
|
||||||
if ((watched == m_cardFrame || watched == m_nameLabel ||
|
|
||||||
watched == m_baseUrlLabel || watched == m_credentialsLabel) &&
|
|
||||||
event && event->type() == QEvent::MouseButtonPress) {
|
|
||||||
emit clicked();
|
|
||||||
}
|
|
||||||
|
|
||||||
return QFrame::eventFilter(watched, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString DanmakuServerListItemWidget::maskedSecret(const QString &secret)
|
|
||||||
{
|
|
||||||
const QString trimmed = secret.trimmed();
|
|
||||||
if (trimmed.isEmpty()) {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
if (trimmed.size() <= 4) {
|
|
||||||
return QStringLiteral("****");
|
|
||||||
}
|
|
||||||
return QStringLiteral("%1****%2")
|
|
||||||
.arg(trimmed.left(2), trimmed.right(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerListItemWidget::refreshCredentialsLabel()
|
|
||||||
{
|
|
||||||
QStringList parts;
|
|
||||||
if (m_server.builtIn &&
|
|
||||||
m_server.contentScope.trimmed().compare(QStringLiteral("anime"),
|
|
||||||
Qt::CaseInsensitive) == 0) {
|
|
||||||
parts.append(tr("Anime only"));
|
|
||||||
}
|
|
||||||
if (!m_server.builtIn && !m_server.description.trimmed().isEmpty()) {
|
|
||||||
parts.append(m_server.description.trimmed());
|
|
||||||
}
|
|
||||||
if (!m_server.builtIn && !m_server.appId.trimmed().isEmpty()) {
|
|
||||||
parts.append(tr("App ID: %1").arg(m_server.appId.trimmed()));
|
|
||||||
}
|
|
||||||
if (!m_server.builtIn && !m_server.appSecret.trimmed().isEmpty()) {
|
|
||||||
parts.append(tr("App Secret: %1")
|
|
||||||
.arg(maskedSecret(m_server.appSecret)));
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString text = parts.join(QStringLiteral(" "));
|
|
||||||
m_credentialsLabel->setVisible(!text.isEmpty());
|
|
||||||
m_credentialsLabel->setText(text);
|
|
||||||
updateGeometry();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerListItemWidget::refreshRemoveIcon()
|
|
||||||
{
|
|
||||||
m_removeButton->setIcon(ThemeManager::getAdaptiveIcon(
|
|
||||||
QStringLiteral(":/svg/light/delete.svg"),
|
|
||||||
m_removable ? activeRemoveIconColor() : inactiveRemoveIconColor()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void DanmakuServerListItemWidget::refreshSelectionState()
|
|
||||||
{
|
|
||||||
m_cardFrame->style()->unpolish(m_cardFrame);
|
|
||||||
m_cardFrame->style()->polish(m_cardFrame);
|
|
||||||
m_cardFrame->update();
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#ifndef DANMAKUSERVERLISTITEMWIDGET_H
|
|
||||||
#define DANMAKUSERVERLISTITEMWIDGET_H
|
|
||||||
|
|
||||||
#include <QFrame>
|
|
||||||
|
|
||||||
#include "models/danmaku/danmakumodels.h"
|
|
||||||
|
|
||||||
class QEvent;
|
|
||||||
class QFrame;
|
|
||||||
class QLabel;
|
|
||||||
class ModernSwitch;
|
|
||||||
class QPushButton;
|
|
||||||
|
|
||||||
class DanmakuServerListItemWidget : public QFrame
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit DanmakuServerListItemWidget(QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
void setServer(const DanmakuServerDefinition &server);
|
|
||||||
void setSelected(bool selected);
|
|
||||||
void setCurrentServer(bool currentServer);
|
|
||||||
void setRemovable(bool removable);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void clicked();
|
|
||||||
void enabledChanged(bool enabled);
|
|
||||||
void removeRequested();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
static QString maskedSecret(const QString &secret);
|
|
||||||
void refreshCredentialsLabel();
|
|
||||||
void refreshRemoveIcon();
|
|
||||||
void refreshSelectionState();
|
|
||||||
|
|
||||||
DanmakuServerDefinition m_server;
|
|
||||||
bool m_removable = true;
|
|
||||||
QFrame *m_cardFrame = nullptr;
|
|
||||||
QLabel *m_nameLabel = nullptr;
|
|
||||||
QLabel *m_baseUrlLabel = nullptr;
|
|
||||||
QLabel *m_credentialsLabel = nullptr;
|
|
||||||
ModernSwitch *m_enabledSwitch = nullptr;
|
|
||||||
QPushButton *m_removeButton = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,562 +0,0 @@
|
|||||||
#include "dashboardsectionorderstrip.h"
|
|
||||||
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QEvent>
|
|
||||||
#include <QGraphicsOpacityEffect>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QMouseEvent>
|
|
||||||
#include <QPropertyAnimation>
|
|
||||||
#include <QResizeEvent>
|
|
||||||
#include <QStyle>
|
|
||||||
#include "../tokens/AnimationTokens.h"
|
|
||||||
#include "../utils/motionprefs.h"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
constexpr int StripHeight = 36;
|
|
||||||
constexpr int ChipHeight = 28;
|
|
||||||
constexpr int ChipSpacing = 8;
|
|
||||||
constexpr int ChipHorizontalPadding = 12;
|
|
||||||
constexpr int DragPreviewYOffset = 5;
|
|
||||||
constexpr int ChipAnimationDuration = AnimationTokens::DurationMs::kNormal;
|
|
||||||
constexpr qreal DragPreviewOpacity = 0.82;
|
|
||||||
}
|
|
||||||
|
|
||||||
DashboardSectionOrderStrip::DashboardSectionOrderStrip(QWidget* parent)
|
|
||||||
: QFrame(parent)
|
|
||||||
{
|
|
||||||
setObjectName("DashboardSectionOrderStrip");
|
|
||||||
setFrameShape(QFrame::NoFrame);
|
|
||||||
setMouseTracking(true);
|
|
||||||
setMinimumHeight(StripHeight);
|
|
||||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
|
||||||
|
|
||||||
m_dropIndicator = new QFrame(this);
|
|
||||||
m_dropIndicator->setObjectName("DashboardSectionOrderDropIndicator");
|
|
||||||
m_dropIndicator->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
||||||
m_dropIndicator->setFixedHeight(ChipHeight);
|
|
||||||
m_dropIndicator->hide();
|
|
||||||
|
|
||||||
m_dragPreview = new QFrame(this);
|
|
||||||
m_dragPreview->setObjectName("DashboardSectionOrderDragPreview");
|
|
||||||
m_dragPreview->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
||||||
m_dragPreview->hide();
|
|
||||||
|
|
||||||
auto* previewLayout = new QHBoxLayout(m_dragPreview);
|
|
||||||
previewLayout->setContentsMargins(ChipHorizontalPadding, 0,
|
|
||||||
ChipHorizontalPadding, 0);
|
|
||||||
previewLayout->setSpacing(0);
|
|
||||||
|
|
||||||
m_dragPreviewLabel = new QLabel(m_dragPreview);
|
|
||||||
m_dragPreviewLabel->setObjectName("DashboardSectionOrderDragPreviewLabel");
|
|
||||||
m_dragPreviewLabel->setAlignment(Qt::AlignCenter);
|
|
||||||
previewLayout->addWidget(m_dragPreviewLabel);
|
|
||||||
|
|
||||||
m_dragPreviewOpacity = new QGraphicsOpacityEffect(m_dragPreview);
|
|
||||||
m_dragPreviewOpacity->setOpacity(DragPreviewOpacity);
|
|
||||||
m_dragPreview->setGraphicsEffect(m_dragPreviewOpacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DashboardSectionOrderStrip::setSectionOrder(
|
|
||||||
const QStringList& order, const QHash<QString, QString>& titles)
|
|
||||||
{
|
|
||||||
if (m_dragging || m_pressedChip) {
|
|
||||||
resetDragState(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_titles = titles;
|
|
||||||
m_order = order;
|
|
||||||
rebuildChips();
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList DashboardSectionOrderStrip::sectionOrder() const
|
|
||||||
{
|
|
||||||
return m_order;
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize DashboardSectionOrderStrip::sizeHint() const
|
|
||||||
{
|
|
||||||
return QSize(fullContentWidth(), StripHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize DashboardSectionOrderStrip::minimumSizeHint() const
|
|
||||||
{
|
|
||||||
return QSize(0, StripHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DashboardSectionOrderStrip::eventFilter(QObject* watched, QEvent* event)
|
|
||||||
{
|
|
||||||
auto* chip = qobject_cast<QFrame*>(watched);
|
|
||||||
if (!chip) {
|
|
||||||
return QFrame::eventFilter(watched, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event->type()) {
|
|
||||||
case QEvent::MouseButtonPress: {
|
|
||||||
auto* mouseEvent = static_cast<QMouseEvent*>(event);
|
|
||||||
if (mouseEvent->button() != Qt::LeftButton) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_pressedChip = chip;
|
|
||||||
m_pressGlobalPos = mouseEvent->globalPosition().toPoint();
|
|
||||||
m_pressChipOffset = mouseEvent->position().toPoint();
|
|
||||||
chip->setCursor(Qt::ClosedHandCursor);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case QEvent::MouseMove: {
|
|
||||||
auto* mouseEvent = static_cast<QMouseEvent*>(event);
|
|
||||||
if (!m_pressedChip || !(mouseEvent->buttons() & Qt::LeftButton)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QPoint globalPos = mouseEvent->globalPosition().toPoint();
|
|
||||||
if (!m_dragging &&
|
|
||||||
(globalPos - m_pressGlobalPos).manhattanLength() >=
|
|
||||||
QApplication::startDragDistance()) {
|
|
||||||
beginDrag(m_pressedChip);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_dragging && chip == m_draggedChip) {
|
|
||||||
updateDrag(globalPos);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case QEvent::MouseButtonRelease: {
|
|
||||||
auto* mouseEvent = static_cast<QMouseEvent*>(event);
|
|
||||||
if (mouseEvent->button() != Qt::LeftButton) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_dragging && chip == m_draggedChip) {
|
|
||||||
finishDrag();
|
|
||||||
} else {
|
|
||||||
chip->setCursor(Qt::OpenHandCursor);
|
|
||||||
resetDragState();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case QEvent::Leave:
|
|
||||||
if (!m_dragging) {
|
|
||||||
chip->setCursor(Qt::OpenHandCursor);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return QFrame::eventFilter(watched, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
QFrame* DashboardSectionOrderStrip::createChip(const QString& title)
|
|
||||||
{
|
|
||||||
auto* chip = new QFrame(this);
|
|
||||||
chip->setObjectName("DashboardSectionOrderChip");
|
|
||||||
chip->setCursor(Qt::OpenHandCursor);
|
|
||||||
chip->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
||||||
chip->installEventFilter(this);
|
|
||||||
|
|
||||||
auto* layout = new QHBoxLayout(chip);
|
|
||||||
layout->setContentsMargins(ChipHorizontalPadding, 0, ChipHorizontalPadding,
|
|
||||||
0);
|
|
||||||
layout->setSpacing(0);
|
|
||||||
|
|
||||||
auto* label = new QLabel(title, chip);
|
|
||||||
label->setObjectName("DashboardSectionOrderChipLabel");
|
|
||||||
label->setAlignment(Qt::AlignCenter);
|
|
||||||
label->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
||||||
layout->addWidget(label);
|
|
||||||
|
|
||||||
chip->setFixedSize(
|
|
||||||
qMax(68, label->sizeHint().width() + ChipHorizontalPadding * 2),
|
|
||||||
ChipHeight);
|
|
||||||
return chip;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DashboardSectionOrderStrip::rebuildChips()
|
|
||||||
{
|
|
||||||
for (ChipItem& item : m_chipItems) {
|
|
||||||
if (item.chip) {
|
|
||||||
item.chip->deleteLater();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_chipItems.clear();
|
|
||||||
|
|
||||||
for (const QString& sectionId : m_order) {
|
|
||||||
const QString title = m_titles.value(sectionId, sectionId);
|
|
||||||
QFrame* chip = createChip(title);
|
|
||||||
m_chipItems.append({sectionId, chip});
|
|
||||||
}
|
|
||||||
|
|
||||||
relayoutChips(false);
|
|
||||||
updateGeometry();
|
|
||||||
}
|
|
||||||
|
|
||||||
int DashboardSectionOrderStrip::indexOfChip(const QFrame* chip) const
|
|
||||||
{
|
|
||||||
for (int index = 0; index < m_chipItems.size(); ++index) {
|
|
||||||
if (m_chipItems[index].chip == chip) {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int DashboardSectionOrderStrip::insertionIndexForPosition(int localX) const
|
|
||||||
{
|
|
||||||
int insertionIndex = 0;
|
|
||||||
|
|
||||||
for (const ChipItem& item : m_chipItems) {
|
|
||||||
if (!item.chip || item.chip == m_draggedChip) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localX < item.chip->geometry().center().x()) {
|
|
||||||
return insertionIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
++insertionIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
return insertionIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DashboardSectionOrderStrip::beginDrag(QFrame* chip)
|
|
||||||
{
|
|
||||||
m_draggedChip = chip;
|
|
||||||
m_dragSourceIndex = indexOfChip(chip);
|
|
||||||
if (m_dragSourceIndex < 0) {
|
|
||||||
resetDragState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_dragging = true;
|
|
||||||
setDraggingChip(chip, true);
|
|
||||||
if (m_dragPreview && m_dragPreviewLabel) {
|
|
||||||
if (auto* label =
|
|
||||||
chip->findChild<QLabel*>("DashboardSectionOrderChipLabel")) {
|
|
||||||
m_dragPreviewLabel->setText(label->text());
|
|
||||||
} else {
|
|
||||||
m_dragPreviewLabel->clear();
|
|
||||||
}
|
|
||||||
m_dragPreview->setFixedSize(chip->size());
|
|
||||||
m_dragPreview->show();
|
|
||||||
m_dragPreview->raise();
|
|
||||||
updateDragPreview(m_pressGlobalPos);
|
|
||||||
}
|
|
||||||
grabMouse();
|
|
||||||
chip->hide();
|
|
||||||
setIndicatorIndex(m_dragSourceIndex, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DashboardSectionOrderStrip::updateDrag(const QPoint& globalPos)
|
|
||||||
{
|
|
||||||
const QPoint localPos = mapFromGlobal(globalPos);
|
|
||||||
updateDragPreview(globalPos);
|
|
||||||
setIndicatorIndex(insertionIndexForPosition(localPos.x()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void DashboardSectionOrderStrip::updateDragPreview(const QPoint& globalPos)
|
|
||||||
{
|
|
||||||
if (!m_dragPreview || !m_dragPreview->isVisible()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QPoint localPos = mapFromGlobal(globalPos);
|
|
||||||
const int previewWidth = m_dragPreview->width();
|
|
||||||
const int previewHeight = m_dragPreview->height();
|
|
||||||
int x = localPos.x() - m_pressChipOffset.x();
|
|
||||||
if (m_dropIndicator->isVisible()) {
|
|
||||||
const int indicatorCenterX = m_dropIndicator->geometry().center().x();
|
|
||||||
const int previewCenterX = x + previewWidth / 2;
|
|
||||||
const int snappedCenterX =
|
|
||||||
qRound(previewCenterX * 0.7 + indicatorCenterX * 0.3);
|
|
||||||
x = snappedCenterX - previewWidth / 2;
|
|
||||||
}
|
|
||||||
x = qBound(0, x, qMax(0, width() - previewWidth));
|
|
||||||
|
|
||||||
const int y = qBound(0, chipY() - DragPreviewYOffset,
|
|
||||||
qMax(0, height() - previewHeight));
|
|
||||||
|
|
||||||
m_dragPreview->move(x, y);
|
|
||||||
m_dragPreview->raise();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DashboardSectionOrderStrip::finishDrag()
|
|
||||||
{
|
|
||||||
if (!m_dragging || m_dragSourceIndex < 0 || !m_draggedChip) {
|
|
||||||
resetDragState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList newOrder = m_order;
|
|
||||||
const QString draggedSectionId = newOrder.takeAt(m_dragSourceIndex);
|
|
||||||
const int targetIndex =
|
|
||||||
qBound(0, m_indicatorIndex, newOrder.size());
|
|
||||||
newOrder.insert(targetIndex, draggedSectionId);
|
|
||||||
|
|
||||||
const bool changed = newOrder != m_order;
|
|
||||||
ChipItem draggedItem = m_chipItems.takeAt(m_dragSourceIndex);
|
|
||||||
m_chipItems.insert(targetIndex, draggedItem);
|
|
||||||
m_order = newOrder;
|
|
||||||
|
|
||||||
resetDragState(true);
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
emit sectionOrderChanged(m_order);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DashboardSectionOrderStrip::resetDragState(bool animateDraggedChip)
|
|
||||||
{
|
|
||||||
if (QWidget::mouseGrabber() == this) {
|
|
||||||
releaseMouse();
|
|
||||||
}
|
|
||||||
|
|
||||||
QPointer<QFrame> draggedChip = m_draggedChip;
|
|
||||||
const QPoint dragPreviewPos =
|
|
||||||
m_dragPreview ? m_dragPreview->pos() : QPoint();
|
|
||||||
|
|
||||||
if (m_draggedChip) {
|
|
||||||
setDraggingChip(m_draggedChip, false);
|
|
||||||
}
|
|
||||||
if (draggedChip) {
|
|
||||||
if (animateDraggedChip && m_dragPreview && m_dragPreview->isVisible()) {
|
|
||||||
draggedChip->move(dragPreviewPos);
|
|
||||||
draggedChip->raise();
|
|
||||||
}
|
|
||||||
draggedChip->show();
|
|
||||||
draggedChip->setCursor(Qt::OpenHandCursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_pressedChip) {
|
|
||||||
m_pressedChip->setCursor(Qt::OpenHandCursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_dragPreview) {
|
|
||||||
m_dragPreview->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
m_pressedChip.clear();
|
|
||||||
m_draggedChip.clear();
|
|
||||||
m_dragSourceIndex = -1;
|
|
||||||
m_indicatorIndex = -1;
|
|
||||||
m_dragging = false;
|
|
||||||
relayoutChips(animateDraggedChip);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DashboardSectionOrderStrip::mouseMoveEvent(QMouseEvent* event)
|
|
||||||
{
|
|
||||||
if (m_dragging) {
|
|
||||||
updateDrag(event->globalPosition().toPoint());
|
|
||||||
event->accept();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFrame::mouseMoveEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DashboardSectionOrderStrip::mouseReleaseEvent(QMouseEvent* event)
|
|
||||||
{
|
|
||||||
if (m_dragging && event->button() == Qt::LeftButton) {
|
|
||||||
finishDrag();
|
|
||||||
event->accept();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFrame::mouseReleaseEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DashboardSectionOrderStrip::resizeEvent(QResizeEvent* event)
|
|
||||||
{
|
|
||||||
QFrame::resizeEvent(event);
|
|
||||||
relayoutChips(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DashboardSectionOrderStrip::setDraggingChip(QFrame* chip, bool dragging)
|
|
||||||
{
|
|
||||||
if (!chip) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
chip->setProperty("dragging", dragging);
|
|
||||||
chip->style()->unpolish(chip);
|
|
||||||
chip->style()->polish(chip);
|
|
||||||
chip->update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DashboardSectionOrderStrip::setIndicatorIndex(int index, bool animate)
|
|
||||||
{
|
|
||||||
const int maxIndex = m_dragging ? qMax(0, m_chipItems.size() - 1)
|
|
||||||
: m_chipItems.size();
|
|
||||||
const int adjustedIndex = qBound(0, index, maxIndex);
|
|
||||||
|
|
||||||
if (m_indicatorIndex == adjustedIndex && m_dropIndicator->isVisible()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_indicatorIndex = adjustedIndex;
|
|
||||||
relayoutChips(animate);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DashboardSectionOrderStrip::relayoutChips(bool animate)
|
|
||||||
{
|
|
||||||
if (m_chipItems.isEmpty()) {
|
|
||||||
if (m_dropIndicator) {
|
|
||||||
stopPosAnimations(m_dropIndicator);
|
|
||||||
m_dropIndicator->hide();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool showDropSlot =
|
|
||||||
m_dragging && m_draggedChip && m_indicatorIndex >= 0;
|
|
||||||
const int slotWidth =
|
|
||||||
(showDropSlot && m_draggedChip) ? m_draggedChip->width() : 0;
|
|
||||||
const int layoutWidth = showDropSlot ? fullContentWidth()
|
|
||||||
: contentWidthForCurrentState();
|
|
||||||
const int startX = qMax(0, width() - layoutWidth);
|
|
||||||
int x = startX;
|
|
||||||
int visibleIndex = 0;
|
|
||||||
bool indicatorPlaced = false;
|
|
||||||
|
|
||||||
for (int index = 0; index < m_chipItems.size(); ++index) {
|
|
||||||
auto* chip = m_chipItems[index].chip;
|
|
||||||
if (!chip || (m_dragging && chip == m_draggedChip)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showDropSlot && m_indicatorIndex == visibleIndex) {
|
|
||||||
m_dropIndicator->setFixedSize(slotWidth, ChipHeight);
|
|
||||||
m_dropIndicator->show();
|
|
||||||
moveWidgetTo(m_dropIndicator, QPoint(x, indicatorY()), animate,
|
|
||||||
ChipAnimationDuration);
|
|
||||||
m_dropIndicator->raise();
|
|
||||||
indicatorPlaced = true;
|
|
||||||
x += slotWidth + ChipSpacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
chip->show();
|
|
||||||
moveWidgetTo(chip, QPoint(x, chipY()), animate, ChipAnimationDuration);
|
|
||||||
x += chip->width();
|
|
||||||
|
|
||||||
const bool hasTrailingVisibleChip = [&]() {
|
|
||||||
for (int next = index + 1; next < m_chipItems.size(); ++next) {
|
|
||||||
if (!m_chipItems[next].chip ||
|
|
||||||
(m_dragging && m_chipItems[next].chip == m_draggedChip)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}();
|
|
||||||
|
|
||||||
if (hasTrailingVisibleChip || (showDropSlot && m_indicatorIndex > visibleIndex)) {
|
|
||||||
x += ChipSpacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
++visibleIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showDropSlot && !indicatorPlaced) {
|
|
||||||
m_dropIndicator->setFixedSize(slotWidth, ChipHeight);
|
|
||||||
m_dropIndicator->show();
|
|
||||||
moveWidgetTo(m_dropIndicator, QPoint(x, indicatorY()), animate,
|
|
||||||
ChipAnimationDuration);
|
|
||||||
m_dropIndicator->raise();
|
|
||||||
indicatorPlaced = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!indicatorPlaced) {
|
|
||||||
stopPosAnimations(m_dropIndicator);
|
|
||||||
m_dropIndicator->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_dragPreview && m_dragPreview->isVisible()) {
|
|
||||||
m_dragPreview->raise();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DashboardSectionOrderStrip::moveWidgetTo(QWidget* widget,
|
|
||||||
const QPoint& targetPos,
|
|
||||||
bool animate, int duration)
|
|
||||||
{
|
|
||||||
if (!widget) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stopPosAnimations(widget);
|
|
||||||
if (!animate || MotionPrefs::reduceMotion() || widget->pos() == targetPos) {
|
|
||||||
widget->move(targetPos);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* animation = new QPropertyAnimation(widget, "pos", widget);
|
|
||||||
animation->setDuration(duration);
|
|
||||||
animation->setEasingCurve(AnimationTokens::Curve::enter());
|
|
||||||
animation->setEndValue(targetPos);
|
|
||||||
animation->start(QAbstractAnimation::DeleteWhenStopped);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DashboardSectionOrderStrip::stopPosAnimations(QWidget* widget) const
|
|
||||||
{
|
|
||||||
if (!widget) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto children = widget->children();
|
|
||||||
for (auto* child : children) {
|
|
||||||
auto* animation = qobject_cast<QPropertyAnimation*>(child);
|
|
||||||
if (!animation || animation->propertyName() != "pos") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
animation->stop();
|
|
||||||
animation->deleteLater();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int DashboardSectionOrderStrip::fullContentWidth() const
|
|
||||||
{
|
|
||||||
if (m_chipItems.isEmpty()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int totalWidth = 0;
|
|
||||||
for (const ChipItem& item : m_chipItems) {
|
|
||||||
if (item.chip) {
|
|
||||||
totalWidth += item.chip->width();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
totalWidth += ChipSpacing * qMax(0, m_chipItems.size() - 1);
|
|
||||||
return totalWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
int DashboardSectionOrderStrip::contentWidthForCurrentState() const
|
|
||||||
{
|
|
||||||
int totalWidth = 0;
|
|
||||||
int visibleCount = 0;
|
|
||||||
for (const ChipItem& item : m_chipItems) {
|
|
||||||
if (!item.chip || (m_dragging && item.chip == m_draggedChip)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
totalWidth += item.chip->width();
|
|
||||||
++visibleCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
totalWidth += ChipSpacing * qMax(0, visibleCount - 1);
|
|
||||||
return totalWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
int DashboardSectionOrderStrip::chipY() const
|
|
||||||
{
|
|
||||||
return qMax(0, (height() - ChipHeight) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
int DashboardSectionOrderStrip::indicatorY() const
|
|
||||||
{
|
|
||||||
return chipY();
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
#ifndef DASHBOARDSECTIONORDERSTRIP_H
|
|
||||||
#define DASHBOARDSECTIONORDERSTRIP_H
|
|
||||||
|
|
||||||
#include <QFrame>
|
|
||||||
#include <QHash>
|
|
||||||
#include <QSize>
|
|
||||||
#include <QPointer>
|
|
||||||
#include <QStringList>
|
|
||||||
|
|
||||||
class QGraphicsOpacityEffect;
|
|
||||||
class QLabel;
|
|
||||||
class QMouseEvent;
|
|
||||||
class QResizeEvent;
|
|
||||||
|
|
||||||
class DashboardSectionOrderStrip : public QFrame {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit DashboardSectionOrderStrip(QWidget* parent = nullptr);
|
|
||||||
|
|
||||||
void setSectionOrder(const QStringList& order,
|
|
||||||
const QHash<QString, QString>& titles);
|
|
||||||
QStringList sectionOrder() const;
|
|
||||||
QSize sizeHint() const override;
|
|
||||||
QSize minimumSizeHint() const override;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void sectionOrderChanged(const QStringList& order);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool eventFilter(QObject* watched, QEvent* event) override;
|
|
||||||
void mouseMoveEvent(QMouseEvent* event) override;
|
|
||||||
void mouseReleaseEvent(QMouseEvent* event) override;
|
|
||||||
void resizeEvent(QResizeEvent* event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct ChipItem {
|
|
||||||
QString sectionId;
|
|
||||||
QFrame* chip = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
QFrame* createChip(const QString& title);
|
|
||||||
void rebuildChips();
|
|
||||||
int indexOfChip(const QFrame* chip) const;
|
|
||||||
int insertionIndexForPosition(int localX) const;
|
|
||||||
void beginDrag(QFrame* chip);
|
|
||||||
void updateDrag(const QPoint& globalPos);
|
|
||||||
void updateDragPreview(const QPoint& globalPos);
|
|
||||||
void finishDrag();
|
|
||||||
void resetDragState(bool animateDraggedChip = false);
|
|
||||||
void setDraggingChip(QFrame* chip, bool dragging);
|
|
||||||
void setIndicatorIndex(int index, bool animate = true);
|
|
||||||
void relayoutChips(bool animate);
|
|
||||||
void moveWidgetTo(QWidget* widget, const QPoint& targetPos, bool animate,
|
|
||||||
int duration = 180);
|
|
||||||
void stopPosAnimations(QWidget* widget) const;
|
|
||||||
int fullContentWidth() const;
|
|
||||||
int contentWidthForCurrentState() const;
|
|
||||||
int chipY() const;
|
|
||||||
int indicatorY() const;
|
|
||||||
|
|
||||||
QFrame* m_dropIndicator = nullptr;
|
|
||||||
QFrame* m_dragPreview = nullptr;
|
|
||||||
QLabel* m_dragPreviewLabel = nullptr;
|
|
||||||
QGraphicsOpacityEffect* m_dragPreviewOpacity = nullptr;
|
|
||||||
QList<ChipItem> m_chipItems;
|
|
||||||
QStringList m_order;
|
|
||||||
QHash<QString, QString> m_titles;
|
|
||||||
QPointer<QFrame> m_pressedChip;
|
|
||||||
QPointer<QFrame> m_draggedChip;
|
|
||||||
QPoint m_pressGlobalPos;
|
|
||||||
QPoint m_pressChipOffset;
|
|
||||||
int m_dragSourceIndex = -1;
|
|
||||||
int m_indicatorIndex = -1;
|
|
||||||
bool m_dragging = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
#include "dashboardsectionorderwidget.h"
|
|
||||||
|
|
||||||
#include "dashboardsectionorderstrip.h"
|
|
||||||
#include "../utils/dashboardsectionorderutils.h"
|
|
||||||
#include <QHash>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
DashboardSectionOrderWidget::DashboardSectionOrderWidget(QWidget* parent)
|
|
||||||
: QWidget(parent)
|
|
||||||
{
|
|
||||||
setObjectName("DashboardSectionOrderWidget");
|
|
||||||
|
|
||||||
auto* layout = new QVBoxLayout(this);
|
|
||||||
layout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
layout->setSpacing(8);
|
|
||||||
|
|
||||||
m_hintLabel = new QLabel(
|
|
||||||
tr("Drag the items below to change the order of home screen sections. "
|
|
||||||
"Left to right matches top to bottom on the home screen."),
|
|
||||||
this);
|
|
||||||
m_hintLabel->setObjectName("SettingsCardDesc");
|
|
||||||
m_hintLabel->setWordWrap(true);
|
|
||||||
layout->addWidget(m_hintLabel);
|
|
||||||
|
|
||||||
m_orderStrip = new DashboardSectionOrderStrip(this);
|
|
||||||
layout->addWidget(m_orderStrip);
|
|
||||||
|
|
||||||
connect(m_orderStrip, &DashboardSectionOrderStrip::sectionOrderChanged, this,
|
|
||||||
&DashboardSectionOrderWidget::sectionOrderChanged);
|
|
||||||
|
|
||||||
setSectionOrder(DashboardSectionOrderUtils::defaultSectionOrder());
|
|
||||||
}
|
|
||||||
|
|
||||||
void DashboardSectionOrderWidget::setSectionOrder(const QStringList& order)
|
|
||||||
{
|
|
||||||
updateSectionOrder(DashboardSectionOrderUtils::normalizeSectionOrder(order));
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList DashboardSectionOrderWidget::sectionOrder() const
|
|
||||||
{
|
|
||||||
if (!m_orderStrip) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return DashboardSectionOrderUtils::normalizeSectionOrder(
|
|
||||||
m_orderStrip->sectionOrder());
|
|
||||||
}
|
|
||||||
|
|
||||||
QString DashboardSectionOrderWidget::titleForSectionId(
|
|
||||||
const QString& sectionId) const
|
|
||||||
{
|
|
||||||
if (sectionId == QLatin1String(
|
|
||||||
DashboardSectionOrderUtils::ContinueWatchingSectionId)) {
|
|
||||||
return tr("Continue Watching");
|
|
||||||
}
|
|
||||||
if (sectionId ==
|
|
||||||
QLatin1String(DashboardSectionOrderUtils::LatestMediaSectionId)) {
|
|
||||||
return tr("Latest Media");
|
|
||||||
}
|
|
||||||
if (sectionId ==
|
|
||||||
QLatin1String(DashboardSectionOrderUtils::RecommendedSectionId)) {
|
|
||||||
return tr("Recommended");
|
|
||||||
}
|
|
||||||
if (sectionId == QLatin1String(
|
|
||||||
DashboardSectionOrderUtils::CompletedWatchingSectionId)) {
|
|
||||||
return tr("Completed Watching");
|
|
||||||
}
|
|
||||||
if (sectionId ==
|
|
||||||
QLatin1String(DashboardSectionOrderUtils::AllLibrariesSectionId)) {
|
|
||||||
return tr("All Libraries");
|
|
||||||
}
|
|
||||||
if (sectionId == QLatin1String(
|
|
||||||
DashboardSectionOrderUtils::
|
|
||||||
EachLibrarySectionsSectionId)) {
|
|
||||||
return tr("Each Library Sections");
|
|
||||||
}
|
|
||||||
|
|
||||||
return sectionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DashboardSectionOrderWidget::updateSectionOrder(const QStringList& order)
|
|
||||||
{
|
|
||||||
if (!m_orderStrip) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<QString, QString> titles;
|
|
||||||
const QStringList normalizedOrder =
|
|
||||||
DashboardSectionOrderUtils::normalizeSectionOrder(order);
|
|
||||||
|
|
||||||
for (const QString& sectionId : normalizedOrder) {
|
|
||||||
titles.insert(sectionId, titleForSectionId(sectionId));
|
|
||||||
}
|
|
||||||
|
|
||||||
m_orderStrip->setSectionOrder(normalizedOrder, titles);
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#ifndef DASHBOARDSECTIONORDERWIDGET_H
|
|
||||||
#define DASHBOARDSECTIONORDERWIDGET_H
|
|
||||||
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QWidget>
|
|
||||||
|
|
||||||
class DashboardSectionOrderStrip;
|
|
||||||
class QLabel;
|
|
||||||
|
|
||||||
class DashboardSectionOrderWidget : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit DashboardSectionOrderWidget(QWidget* parent = nullptr);
|
|
||||||
|
|
||||||
void setSectionOrder(const QStringList& order);
|
|
||||||
QStringList sectionOrder() const;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void sectionOrderChanged(const QStringList& order);
|
|
||||||
|
|
||||||
private:
|
|
||||||
QString titleForSectionId(const QString& sectionId) const;
|
|
||||||
void updateSectionOrder(const QStringList& order);
|
|
||||||
|
|
||||||
QLabel* m_hintLabel = nullptr;
|
|
||||||
DashboardSectionOrderStrip* m_orderStrip = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,459 +0,0 @@
|
|||||||
#include "detailactionwidget.h"
|
|
||||||
#include "../managers/externalplayerdetector.h"
|
|
||||||
#include "../managers/thememanager.h"
|
|
||||||
#include "../utils/mediasourcepreferenceutils.h"
|
|
||||||
#include "../utils/playerpreferenceutils.h"
|
|
||||||
#include "../utils/popfeedback.h"
|
|
||||||
#include "modernmenubutton.h"
|
|
||||||
#include "splitplayerbutton.h"
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QProgressBar>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QSet>
|
|
||||||
#include <QStyle>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
#include <config/config_keys.h>
|
|
||||||
#include <config/configstore.h>
|
|
||||||
|
|
||||||
DetailActionWidget::DetailActionWidget(QWidget *parent) : QWidget(parent) {
|
|
||||||
auto *mainLayout = new QVBoxLayout(this);
|
|
||||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
mainLayout->setSpacing(4);
|
|
||||||
|
|
||||||
|
|
||||||
auto *actionsLayout = new QHBoxLayout();
|
|
||||||
actionsLayout->setSpacing(12);
|
|
||||||
actionsLayout->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
|
||||||
|
|
||||||
m_resumeBtn = new QPushButton(tr("▶ Resume"), this);
|
|
||||||
m_resumeBtn->setObjectName("detail-resume-btn");
|
|
||||||
m_resumeBtn->setCursor(Qt::PointingHandCursor);
|
|
||||||
m_resumeBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
||||||
|
|
||||||
m_playBtn = new QPushButton(tr("▶ Play"), this);
|
|
||||||
m_playBtn->setObjectName("detail-play-btn");
|
|
||||||
m_playBtn->setCursor(Qt::PointingHandCursor);
|
|
||||||
m_playBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
||||||
|
|
||||||
m_favBtn = new QPushButton(this);
|
|
||||||
m_favBtn->setObjectName("detail-fav-btn");
|
|
||||||
m_favBtn->setIcon(QIcon(":/svg/light/heart.svg"));
|
|
||||||
m_favBtn->setIconSize(QSize(20, 20));
|
|
||||||
m_favBtn->setFixedSize(36, 36);
|
|
||||||
m_favBtn->setCursor(Qt::PointingHandCursor);
|
|
||||||
|
|
||||||
m_playedBtn = new QPushButton(this);
|
|
||||||
m_playedBtn->setObjectName("detail-played-btn");
|
|
||||||
m_playedBtn->setIconSize(QSize(20, 20));
|
|
||||||
m_playedBtn->setFixedSize(36, 36);
|
|
||||||
m_playedBtn->setCursor(Qt::PointingHandCursor);
|
|
||||||
|
|
||||||
m_progressWidget = new QWidget(this);
|
|
||||||
m_progressWidget->setMaximumWidth(450);
|
|
||||||
auto *progressLayout = new QHBoxLayout(m_progressWidget);
|
|
||||||
progressLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
progressLayout->setSpacing(10);
|
|
||||||
|
|
||||||
m_progressBar = new QProgressBar(m_progressWidget);
|
|
||||||
m_progressBar->setObjectName("detail-progress-bar");
|
|
||||||
m_progressBar->setTextVisible(false);
|
|
||||||
|
|
||||||
m_remainingTimeLabel = new QLabel(m_progressWidget);
|
|
||||||
m_remainingTimeLabel->setObjectName("detail-remaining-time");
|
|
||||||
|
|
||||||
progressLayout->addWidget(m_progressBar, 1);
|
|
||||||
progressLayout->addWidget(m_remainingTimeLabel);
|
|
||||||
|
|
||||||
|
|
||||||
m_extPlayerBtn = new SplitPlayerButton(this);
|
|
||||||
m_extPlayerBtn->setObjectName("detail-ext-player-btn");
|
|
||||||
m_extPlayerBtn->setIconOnly(true);
|
|
||||||
m_extPlayerBtn->hide();
|
|
||||||
|
|
||||||
connect(m_extPlayerBtn, &SplitPlayerButton::playRequested, this,
|
|
||||||
&DetailActionWidget::externalPlayRequested);
|
|
||||||
connect(m_extPlayerBtn, &SplitPlayerButton::playerSelected, this,
|
|
||||||
&DetailActionWidget::externalPlayRequested);
|
|
||||||
|
|
||||||
actionsLayout->addWidget(m_resumeBtn);
|
|
||||||
actionsLayout->addWidget(m_playBtn);
|
|
||||||
actionsLayout->addWidget(m_extPlayerBtn);
|
|
||||||
actionsLayout->addWidget(m_favBtn);
|
|
||||||
actionsLayout->addWidget(m_playedBtn);
|
|
||||||
actionsLayout->addWidget(m_progressWidget);
|
|
||||||
actionsLayout->addStretch();
|
|
||||||
|
|
||||||
|
|
||||||
auto *streamSelectorsLayout = new QHBoxLayout();
|
|
||||||
streamSelectorsLayout->setSpacing(6);
|
|
||||||
streamSelectorsLayout->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
|
||||||
|
|
||||||
m_versionComboBox = new ModernMenuButton(this);
|
|
||||||
m_audioComboBox = new ModernMenuButton(this);
|
|
||||||
m_subtitleComboBox = new ModernMenuButton(this);
|
|
||||||
|
|
||||||
streamSelectorsLayout->addWidget(m_versionComboBox);
|
|
||||||
streamSelectorsLayout->addWidget(m_audioComboBox);
|
|
||||||
streamSelectorsLayout->addWidget(m_subtitleComboBox);
|
|
||||||
streamSelectorsLayout->addStretch();
|
|
||||||
|
|
||||||
mainLayout->addLayout(actionsLayout);
|
|
||||||
mainLayout->addLayout(streamSelectorsLayout);
|
|
||||||
|
|
||||||
connect(m_playBtn, &QPushButton::clicked, this,
|
|
||||||
&DetailActionWidget::playRequested);
|
|
||||||
connect(m_resumeBtn, &QPushButton::clicked, this,
|
|
||||||
&DetailActionWidget::resumeRequested);
|
|
||||||
connect(m_favBtn, &QPushButton::clicked, this, [this]() {
|
|
||||||
PopFeedback::play(m_favBtn, [btn = m_favBtn](qreal scale) {
|
|
||||||
btn->setIconSize(QSize(qRound(20 * scale), qRound(20 * scale)));
|
|
||||||
});
|
|
||||||
emit favoriteRequested();
|
|
||||||
});
|
|
||||||
connect(m_playedBtn, &QPushButton::clicked, this, [this]() {
|
|
||||||
PopFeedback::play(m_playedBtn, [btn = m_playedBtn](qreal scale) {
|
|
||||||
btn->setIconSize(QSize(qRound(20 * scale), qRound(20 * scale)));
|
|
||||||
});
|
|
||||||
emit playedToggleRequested();
|
|
||||||
});
|
|
||||||
connect(m_versionComboBox, &ModernMenuButton::currentIndexChanged, this,
|
|
||||||
&DetailActionWidget::sourceVersionChanged);
|
|
||||||
|
|
||||||
clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailActionWidget::clear() {
|
|
||||||
m_resumeBtn->hide();
|
|
||||||
m_playBtn->setText(tr("▶ Play"));
|
|
||||||
m_progressWidget->hide();
|
|
||||||
|
|
||||||
m_versionComboBox->blockSignals(true);
|
|
||||||
m_versionComboBox->clear();
|
|
||||||
m_versionComboBox->hide();
|
|
||||||
m_versionComboBox->blockSignals(false);
|
|
||||||
|
|
||||||
m_audioComboBox->blockSignals(true);
|
|
||||||
m_audioComboBox->clear();
|
|
||||||
m_audioComboBox->hide();
|
|
||||||
m_audioComboBox->blockSignals(false);
|
|
||||||
|
|
||||||
m_subtitleComboBox->blockSignals(true);
|
|
||||||
m_subtitleComboBox->clear();
|
|
||||||
m_subtitleComboBox->hide();
|
|
||||||
m_subtitleComboBox->blockSignals(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailActionWidget::setupNormalMode(const MediaItem &item) {
|
|
||||||
m_playBtn->setEnabled(true);
|
|
||||||
if (item.userData.playedPercentage > 0 &&
|
|
||||||
item.userData.playedPercentage < 100) {
|
|
||||||
m_resumeBtn->show();
|
|
||||||
m_playBtn->setText(tr("↺ Play from Beginning"));
|
|
||||||
|
|
||||||
m_progressBar->setValue(static_cast<int>(item.userData.playedPercentage));
|
|
||||||
long long remainingTicks =
|
|
||||||
item.runTimeTicks - item.userData.playbackPositionTicks;
|
|
||||||
if (remainingTicks > 0) {
|
|
||||||
m_remainingTimeLabel->setText(
|
|
||||||
tr("%1 remaining").arg(formatRunTime(remainingTicks)));
|
|
||||||
} else {
|
|
||||||
m_remainingTimeLabel->clear();
|
|
||||||
}
|
|
||||||
m_progressWidget->show();
|
|
||||||
} else {
|
|
||||||
m_resumeBtn->hide();
|
|
||||||
m_playBtn->setText(tr("▶ Play"));
|
|
||||||
m_progressWidget->hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailActionWidget::setSeriesLoadingMode() {
|
|
||||||
m_resumeBtn->hide();
|
|
||||||
m_playBtn->setText(tr("▶ Play"));
|
|
||||||
m_playBtn->setEnabled(false);
|
|
||||||
m_progressWidget->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailActionWidget::setupSeriesMode(const MediaItem &nextUpItem,
|
|
||||||
const QString &epTag) {
|
|
||||||
m_playBtn->setEnabled(true);
|
|
||||||
if (nextUpItem.userData.playbackPositionTicks > 0) {
|
|
||||||
m_resumeBtn->show();
|
|
||||||
m_resumeBtn->setText(tr("▶ Resume %1").arg(epTag));
|
|
||||||
m_playBtn->setText(tr("↺ Restart %1").arg(epTag));
|
|
||||||
|
|
||||||
if (nextUpItem.userData.playedPercentage > 0) {
|
|
||||||
m_progressBar->setValue(
|
|
||||||
static_cast<int>(nextUpItem.userData.playedPercentage));
|
|
||||||
long long remainingTicks =
|
|
||||||
nextUpItem.runTimeTicks - nextUpItem.userData.playbackPositionTicks;
|
|
||||||
if (remainingTicks > 0) {
|
|
||||||
m_remainingTimeLabel->setText(
|
|
||||||
tr("%1 remaining").arg(formatRunTime(remainingTicks)));
|
|
||||||
} else {
|
|
||||||
m_remainingTimeLabel->clear();
|
|
||||||
}
|
|
||||||
m_progressWidget->show();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
m_resumeBtn->hide();
|
|
||||||
m_playBtn->setText(tr("▶ Play %1").arg(epTag));
|
|
||||||
m_progressWidget->hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailActionWidget::setFavoriteState(bool isFavorite) {
|
|
||||||
m_favBtn->setProperty("isFavorite", isFavorite);
|
|
||||||
m_favBtn->setIcon(QIcon(isFavorite ? ":/svg/light/heart-fill.svg"
|
|
||||||
: ":/svg/light/heart-outline.svg"));
|
|
||||||
m_favBtn->style()->unpolish(m_favBtn);
|
|
||||||
m_favBtn->style()->polish(m_favBtn);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailActionWidget::setPlayedState(bool played) {
|
|
||||||
m_playedBtn->setProperty("played", played);
|
|
||||||
const QString themeDir =
|
|
||||||
ThemeManager::instance()->isDarkMode() ? "dark" : "light";
|
|
||||||
if (played) {
|
|
||||||
m_playedBtn->setIcon(QIcon(":/svg/dark/played-check.svg"));
|
|
||||||
} else {
|
|
||||||
m_playedBtn->setIcon(
|
|
||||||
QIcon(QString(":/svg/%1/unplayed-check.svg").arg(themeDir)));
|
|
||||||
}
|
|
||||||
m_playedBtn->style()->unpolish(m_playedBtn);
|
|
||||||
m_playedBtn->style()->polish(m_playedBtn);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailActionWidget::setSources(const QList<MediaSourceInfo> &sources,
|
|
||||||
int currentIndex) {
|
|
||||||
m_versionComboBox->blockSignals(true);
|
|
||||||
m_versionComboBox->clear();
|
|
||||||
|
|
||||||
if (!sources.isEmpty()) {
|
|
||||||
for (int i = 0; i < sources.size(); ++i) {
|
|
||||||
const auto &src = sources[i];
|
|
||||||
QString versionName =
|
|
||||||
src.name.isEmpty() ? tr("Version %1").arg(i + 1) : src.name;
|
|
||||||
QString videoInfo = tr("Unknown Video");
|
|
||||||
for (const auto &stream : src.mediaStreams) {
|
|
||||||
if (stream.type == "Video") {
|
|
||||||
QStringList parts;
|
|
||||||
if (stream.width > 0)
|
|
||||||
parts << QString("%1x%2").arg(stream.width).arg(stream.height);
|
|
||||||
if (!stream.codec.isEmpty())
|
|
||||||
parts << stream.codec.toUpper();
|
|
||||||
if (stream.realFrameRate > 0)
|
|
||||||
parts << QString::number(stream.realFrameRate, 'f', 1) + " fps";
|
|
||||||
if (stream.bitRate > 0)
|
|
||||||
parts << QString("%1 Mbps").arg(stream.bitRate / 1000000.0, 0, 'f',
|
|
||||||
1);
|
|
||||||
if (!parts.isEmpty())
|
|
||||||
videoInfo = parts.join(" · ");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_versionComboBox->addItem("🎞 " + versionName, videoInfo, QString(),
|
|
||||||
src.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
int bestIndex = currentIndex;
|
|
||||||
if (sources.size() > 1) {
|
|
||||||
bestIndex = MediaSourcePreferenceUtils::resolvePreferredMediaSourceIndex(
|
|
||||||
sources,
|
|
||||||
ConfigStore::instance()
|
|
||||||
->get<QString>(ConfigKeys::PlayerPreferredVersion)
|
|
||||||
.trimmed());
|
|
||||||
}
|
|
||||||
|
|
||||||
m_versionComboBox->setCurrentIndex(bestIndex);
|
|
||||||
m_versionComboBox->show();
|
|
||||||
} else {
|
|
||||||
m_versionComboBox->hide();
|
|
||||||
}
|
|
||||||
m_versionComboBox->blockSignals(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailActionWidget::setStreams(const MediaSourceInfo &source) {
|
|
||||||
m_audioComboBox->blockSignals(true);
|
|
||||||
m_subtitleComboBox->blockSignals(true);
|
|
||||||
m_audioComboBox->clear();
|
|
||||||
m_subtitleComboBox->clear();
|
|
||||||
|
|
||||||
m_subtitleComboBox->addItem("🚫 " + tr("No Subtitles"), QString(),
|
|
||||||
tr("Disable subtitle display"), -1);
|
|
||||||
|
|
||||||
|
|
||||||
QString prefAudioLang = ConfigStore::instance()->get<QString>(
|
|
||||||
ConfigKeys::PlayerAudioLang, "auto");
|
|
||||||
QString prefSubLang =
|
|
||||||
ConfigStore::instance()->get<QString>(ConfigKeys::PlayerSubLang, "auto");
|
|
||||||
const int preferredAudioStreamIndex =
|
|
||||||
PlayerPreferenceUtils::findPreferredStreamIndex(
|
|
||||||
source.mediaStreams, "Audio", prefAudioLang);
|
|
||||||
const int preferredSubtitleStreamIndex =
|
|
||||||
PlayerPreferenceUtils::findPreferredStreamIndex(
|
|
||||||
source.mediaStreams, "Subtitle", prefSubLang);
|
|
||||||
const bool subtitleDisabled =
|
|
||||||
PlayerPreferenceUtils::isSubtitleDisabled(prefSubLang);
|
|
||||||
|
|
||||||
int defaultAudioIdx = 0;
|
|
||||||
int defaultSubIdx = 0;
|
|
||||||
bool subMatchedByPref = false;
|
|
||||||
bool subMatchedByDefault = false;
|
|
||||||
|
|
||||||
for (const auto &stream : source.mediaStreams) {
|
|
||||||
if (stream.type == "Audio") {
|
|
||||||
QString title =
|
|
||||||
stream.displayTitle.isEmpty() ? stream.language : stream.displayTitle;
|
|
||||||
if (title.isEmpty())
|
|
||||||
title = tr("Audio Track");
|
|
||||||
QStringList parts;
|
|
||||||
if (!stream.codec.isEmpty())
|
|
||||||
parts << stream.codec.toUpper();
|
|
||||||
if (stream.channels > 0)
|
|
||||||
parts << QString("%1 ch").arg(stream.channels);
|
|
||||||
if (stream.sampleRate > 0)
|
|
||||||
parts << QString("%1 kHz").arg(stream.sampleRate / 1000.0, 0, 'f', 1);
|
|
||||||
if (stream.bitRate > 0)
|
|
||||||
parts << QString("%1 kbps").arg(stream.bitRate / 1000);
|
|
||||||
|
|
||||||
|
|
||||||
QStringList secondLineParts;
|
|
||||||
if (!stream.title.isEmpty())
|
|
||||||
secondLineParts << stream.title;
|
|
||||||
if (!parts.isEmpty())
|
|
||||||
secondLineParts << parts.join(" · ");
|
|
||||||
m_audioComboBox->addItem("🔊 " + title, QString(),
|
|
||||||
secondLineParts.join(" · "), stream.index);
|
|
||||||
|
|
||||||
int curIdx = m_audioComboBox->count() - 1;
|
|
||||||
if (preferredAudioStreamIndex >= 0 &&
|
|
||||||
stream.index == preferredAudioStreamIndex) {
|
|
||||||
defaultAudioIdx = curIdx;
|
|
||||||
} else if (preferredAudioStreamIndex < 0 && stream.isDefault) {
|
|
||||||
defaultAudioIdx = curIdx;
|
|
||||||
}
|
|
||||||
} else if (stream.type == "Subtitle") {
|
|
||||||
QString title =
|
|
||||||
stream.displayTitle.isEmpty() ? stream.language : stream.displayTitle;
|
|
||||||
if (title.isEmpty())
|
|
||||||
title = tr("Subtitle");
|
|
||||||
QStringList parts;
|
|
||||||
if (!stream.codec.isEmpty())
|
|
||||||
parts << stream.codec.toUpper();
|
|
||||||
if (stream.isForced)
|
|
||||||
parts << tr("Forced");
|
|
||||||
if (stream.isExternal)
|
|
||||||
parts << tr("External");
|
|
||||||
if (stream.isHearingImpaired)
|
|
||||||
parts << tr("SDH");
|
|
||||||
|
|
||||||
|
|
||||||
QStringList secondLineParts;
|
|
||||||
if (!stream.title.isEmpty())
|
|
||||||
secondLineParts << stream.title;
|
|
||||||
if (!parts.isEmpty())
|
|
||||||
secondLineParts << parts.join(" · ");
|
|
||||||
m_subtitleComboBox->addItem("💬 " + title, QString(),
|
|
||||||
secondLineParts.join(" · "), stream.index);
|
|
||||||
|
|
||||||
int curIdx = m_subtitleComboBox->count() - 1;
|
|
||||||
if (preferredSubtitleStreamIndex >= 0 &&
|
|
||||||
stream.index == preferredSubtitleStreamIndex) {
|
|
||||||
defaultSubIdx = curIdx;
|
|
||||||
subMatchedByPref = true;
|
|
||||||
} else if (preferredSubtitleStreamIndex < 0 && !subMatchedByPref &&
|
|
||||||
stream.isDefault) {
|
|
||||||
defaultSubIdx = curIdx;
|
|
||||||
subMatchedByDefault = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (subtitleDisabled) {
|
|
||||||
defaultSubIdx = 0;
|
|
||||||
} else if (!subMatchedByPref && !subMatchedByDefault &&
|
|
||||||
m_subtitleComboBox->count() > 1) {
|
|
||||||
|
|
||||||
|
|
||||||
defaultSubIdx = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_audioComboBox->count() > 0) {
|
|
||||||
m_audioComboBox->setCurrentIndex(defaultAudioIdx);
|
|
||||||
m_audioComboBox->show();
|
|
||||||
} else {
|
|
||||||
m_audioComboBox->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_subtitleComboBox->count() > 1) {
|
|
||||||
m_subtitleComboBox->setCurrentIndex(defaultSubIdx);
|
|
||||||
m_subtitleComboBox->show();
|
|
||||||
} else {
|
|
||||||
m_subtitleComboBox->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
m_audioComboBox->blockSignals(false);
|
|
||||||
m_subtitleComboBox->blockSignals(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
int DetailActionWidget::currentSourceIndex() const {
|
|
||||||
return qMax(0, m_versionComboBox->currentIndex());
|
|
||||||
}
|
|
||||||
int DetailActionWidget::currentAudioIndex() const {
|
|
||||||
return m_audioComboBox->isVisible() ? m_audioComboBox->currentData().toInt()
|
|
||||||
: -1;
|
|
||||||
}
|
|
||||||
int DetailActionWidget::currentSubtitleIndex() const {
|
|
||||||
return m_subtitleComboBox->isVisible()
|
|
||||||
? m_subtitleComboBox->currentData().toInt()
|
|
||||||
: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString DetailActionWidget::formatRunTime(long long ticks) {
|
|
||||||
long long totalSeconds = ticks / 10000000;
|
|
||||||
long long hours = totalSeconds / 3600;
|
|
||||||
long long minutes = (totalSeconds % 3600) / 60;
|
|
||||||
if (hours > 0)
|
|
||||||
return QString(tr("%1 hr %2 min")).arg(hours).arg(minutes);
|
|
||||||
return QString(tr("%1 min")).arg(minutes);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailActionWidget::refreshExtPlayerButton() {
|
|
||||||
bool extEnabled =
|
|
||||||
ConfigStore::instance()->get<bool>(ConfigKeys::ExtPlayerEnable, false);
|
|
||||||
if (!extEnabled) {
|
|
||||||
m_extPlayerBtn->hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString currentPath =
|
|
||||||
ConfigStore::instance()->get<QString>(ConfigKeys::ExtPlayerPath);
|
|
||||||
|
|
||||||
|
|
||||||
QList<DetectedPlayer> allPlayers = ExternalPlayerDetector::loadFromConfig();
|
|
||||||
|
|
||||||
|
|
||||||
QSet<QString> knownPaths;
|
|
||||||
for (const auto &p : allPlayers)
|
|
||||||
knownPaths.insert(p.path);
|
|
||||||
if (!currentPath.isEmpty() && currentPath != "custom" &&
|
|
||||||
!knownPaths.contains(currentPath) && QFileInfo::exists(currentPath)) {
|
|
||||||
allPlayers.prepend({QFileInfo(currentPath).baseName(), currentPath});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allPlayers.isEmpty()) {
|
|
||||||
m_extPlayerBtn->hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
QString activePlayerPath = currentPath;
|
|
||||||
if (activePlayerPath.isEmpty() || activePlayerPath == "custom") {
|
|
||||||
activePlayerPath = allPlayers.first().path;
|
|
||||||
}
|
|
||||||
m_extPlayerBtn->setPlayers(allPlayers, activePlayerPath);
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
#ifndef DETAILACTIONWIDGET_H
|
|
||||||
#define DETAILACTIONWIDGET_H
|
|
||||||
|
|
||||||
#include <QWidget>
|
|
||||||
#include "models/media/mediaitem.h"
|
|
||||||
|
|
||||||
class QPushButton;
|
|
||||||
class QProgressBar;
|
|
||||||
class QLabel;
|
|
||||||
class ModernMenuButton;
|
|
||||||
class SplitPlayerButton;
|
|
||||||
|
|
||||||
class DetailActionWidget : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit DetailActionWidget(QWidget* parent = nullptr);
|
|
||||||
|
|
||||||
void setupNormalMode(const MediaItem& item);
|
|
||||||
void setupSeriesMode(const MediaItem& nextUpItem, const QString& epTag);
|
|
||||||
void setSeriesLoadingMode();
|
|
||||||
|
|
||||||
void setFavoriteState(bool isFavorite);
|
|
||||||
void setPlayedState(bool played);
|
|
||||||
void setSources(const QList<MediaSourceInfo>& sources, int currentIndex = 0);
|
|
||||||
void setStreams(const MediaSourceInfo& source);
|
|
||||||
void clear();
|
|
||||||
|
|
||||||
int currentSourceIndex() const;
|
|
||||||
int currentAudioIndex() const;
|
|
||||||
int currentSubtitleIndex() const;
|
|
||||||
|
|
||||||
|
|
||||||
void refreshExtPlayerButton();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void playRequested();
|
|
||||||
void resumeRequested();
|
|
||||||
void favoriteRequested();
|
|
||||||
void playedToggleRequested();
|
|
||||||
void sourceVersionChanged(int index);
|
|
||||||
void externalPlayRequested(const QString &playerPath);
|
|
||||||
|
|
||||||
private:
|
|
||||||
QString formatRunTime(long long ticks);
|
|
||||||
|
|
||||||
QPushButton* m_resumeBtn;
|
|
||||||
QPushButton* m_playBtn;
|
|
||||||
QPushButton* m_favBtn;
|
|
||||||
QPushButton* m_playedBtn;
|
|
||||||
|
|
||||||
QWidget* m_progressWidget;
|
|
||||||
QProgressBar* m_progressBar;
|
|
||||||
QLabel* m_remainingTimeLabel;
|
|
||||||
|
|
||||||
ModernMenuButton* m_versionComboBox;
|
|
||||||
ModernMenuButton* m_audioComboBox;
|
|
||||||
ModernMenuButton* m_subtitleComboBox;
|
|
||||||
|
|
||||||
|
|
||||||
SplitPlayerButton* m_extPlayerBtn;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,494 +0,0 @@
|
|||||||
#include "detailbottominfowidget.h"
|
|
||||||
#include "detailtagbutton.h"
|
|
||||||
#include "flowlayout.h"
|
|
||||||
#include "horizontalwidgetgallery.h"
|
|
||||||
#include <QDesktopServices>
|
|
||||||
#include <QEvent>
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QMargins>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QShowEvent>
|
|
||||||
#include <QTimer>
|
|
||||||
#include <QUrl>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
|
|
||||||
DetailBottomInfoWidget::DetailBottomInfoWidget(QWidget *parent)
|
|
||||||
: QWidget(parent) {
|
|
||||||
auto *mainLayout = new QVBoxLayout(this);
|
|
||||||
mainLayout->setContentsMargins(40, 8, 40, 0);
|
|
||||||
mainLayout->setSpacing(12);
|
|
||||||
|
|
||||||
|
|
||||||
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
|
|
||||||
|
|
||||||
m_tagsBottomTitle = new QLabel(tr("Tags"), this);
|
|
||||||
m_tagsBottomTitle->setObjectName("detail-section-title");
|
|
||||||
m_tagsBottomWidget = new QWidget(this);
|
|
||||||
m_tagsBottomLayout = new FlowLayout(m_tagsBottomWidget, 0, 8, 8);
|
|
||||||
|
|
||||||
m_studiosTitle = new QLabel(tr("Studios"), this);
|
|
||||||
m_studiosTitle->setObjectName("detail-section-title");
|
|
||||||
m_studiosWidget = new QWidget(this);
|
|
||||||
m_studiosLayout = new FlowLayout(m_studiosWidget, 0, 8, 8);
|
|
||||||
|
|
||||||
|
|
||||||
m_externalLinksTitle = new QLabel(tr("External Links"), this);
|
|
||||||
m_externalLinksTitle->setObjectName("detail-section-title");
|
|
||||||
m_externalLinksWidget = new QWidget(this);
|
|
||||||
m_externalLinksLayout = new FlowLayout(m_externalLinksWidget, 0, 8, 8);
|
|
||||||
|
|
||||||
m_mediaInfoTitle = new QLabel(tr("Media Info"), this);
|
|
||||||
m_mediaInfoTitle->setObjectName("detail-section-title");
|
|
||||||
|
|
||||||
m_fileInfoWidget = new QWidget(this);
|
|
||||||
auto *fileInfoLayout = new QVBoxLayout(m_fileInfoWidget);
|
|
||||||
fileInfoLayout->setContentsMargins(0, 0, 0, 4);
|
|
||||||
fileInfoLayout->setSpacing(6);
|
|
||||||
|
|
||||||
m_filePathLabel = new QLabel(m_fileInfoWidget);
|
|
||||||
m_filePathLabel->setObjectName("detail-filepath");
|
|
||||||
m_filePathLabel->setWordWrap(true);
|
|
||||||
m_filePathLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
|
|
||||||
|
|
||||||
m_fileMetaLabel = new QLabel(m_fileInfoWidget);
|
|
||||||
m_fileMetaLabel->setObjectName("detail-filemeta");
|
|
||||||
|
|
||||||
fileInfoLayout->addWidget(m_filePathLabel);
|
|
||||||
fileInfoLayout->addWidget(m_fileMetaLabel);
|
|
||||||
|
|
||||||
m_mediaInfoGallery = new HorizontalWidgetGallery(this);
|
|
||||||
m_mediaInfoGallery->setObjectName("detail-media-info-gallery");
|
|
||||||
|
|
||||||
m_tagsBottomWrapper = wrapMaxWidth(m_tagsBottomWidget, 1200);
|
|
||||||
mainLayout->addWidget(m_tagsBottomTitle);
|
|
||||||
mainLayout->addWidget(m_tagsBottomWrapper);
|
|
||||||
|
|
||||||
m_studiosWrapper = wrapMaxWidth(m_studiosWidget, 1200);
|
|
||||||
mainLayout->addWidget(m_studiosTitle);
|
|
||||||
mainLayout->addWidget(m_studiosWrapper);
|
|
||||||
|
|
||||||
|
|
||||||
m_externalLinksWrapper = wrapMaxWidth(m_externalLinksWidget, 1200);
|
|
||||||
mainLayout->addWidget(m_externalLinksTitle);
|
|
||||||
mainLayout->addWidget(m_externalLinksWrapper);
|
|
||||||
|
|
||||||
mainLayout->addWidget(m_mediaInfoTitle);
|
|
||||||
mainLayout->addWidget(m_fileInfoWidget);
|
|
||||||
mainLayout->addWidget(m_mediaInfoGallery);
|
|
||||||
|
|
||||||
auto watchFlowGeometry = [this](QWidget *widget, QWidget *wrapper) {
|
|
||||||
if (widget)
|
|
||||||
widget->installEventFilter(this);
|
|
||||||
if (wrapper)
|
|
||||||
wrapper->installEventFilter(this);
|
|
||||||
};
|
|
||||||
watchFlowGeometry(m_tagsBottomWidget, m_tagsBottomWrapper);
|
|
||||||
watchFlowGeometry(m_studiosWidget, m_studiosWrapper);
|
|
||||||
watchFlowGeometry(m_externalLinksWidget, m_externalLinksWrapper);
|
|
||||||
|
|
||||||
clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
QWidget *DetailBottomInfoWidget::wrapMaxWidth(QWidget *child, int maxW) {
|
|
||||||
child->setMaximumWidth(maxW);
|
|
||||||
child->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
|
|
||||||
QWidget *wrapper = new QWidget(this);
|
|
||||||
wrapper->setObjectName("detail-maxWidth-wrapper");
|
|
||||||
wrapper->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
|
|
||||||
auto *hl = new QHBoxLayout(wrapper);
|
|
||||||
hl->setContentsMargins(0, 0, 0, 0);
|
|
||||||
hl->setSpacing(0);
|
|
||||||
hl->addWidget(child, 1);
|
|
||||||
hl->addStretch(0);
|
|
||||||
return wrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailBottomInfoWidget::clear() {
|
|
||||||
clearLayout(m_tagsBottomLayout);
|
|
||||||
clearLayout(m_studiosLayout);
|
|
||||||
clearLayout(m_externalLinksLayout);
|
|
||||||
m_mediaInfoGallery->clear();
|
|
||||||
|
|
||||||
m_tagsBottomTitle->hide();
|
|
||||||
m_tagsBottomWrapper->hide();
|
|
||||||
m_studiosTitle->hide();
|
|
||||||
m_studiosWrapper->hide();
|
|
||||||
m_externalLinksTitle->hide();
|
|
||||||
m_externalLinksWrapper->hide();
|
|
||||||
m_mediaInfoTitle->hide();
|
|
||||||
m_fileInfoWidget->hide();
|
|
||||||
m_mediaInfoGallery->hide();
|
|
||||||
updateFlowLayoutHeights();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailBottomInfoWidget::resizeEvent(QResizeEvent *event) {
|
|
||||||
QWidget::resizeEvent(event);
|
|
||||||
scheduleFlowLayoutHeightUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailBottomInfoWidget::showEvent(QShowEvent *event) {
|
|
||||||
QWidget::showEvent(event);
|
|
||||||
scheduleFlowLayoutHeightUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DetailBottomInfoWidget::eventFilter(QObject *watched, QEvent *event) {
|
|
||||||
const bool watchesFlowGeometry =
|
|
||||||
watched == m_tagsBottomWidget || watched == m_tagsBottomWrapper ||
|
|
||||||
watched == m_studiosWidget || watched == m_studiosWrapper ||
|
|
||||||
watched == m_externalLinksWidget ||
|
|
||||||
watched == m_externalLinksWrapper;
|
|
||||||
|
|
||||||
if (watchesFlowGeometry) {
|
|
||||||
switch (event->type()) {
|
|
||||||
case QEvent::LayoutRequest:
|
|
||||||
case QEvent::Resize:
|
|
||||||
case QEvent::Show:
|
|
||||||
scheduleFlowLayoutHeightUpdate();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return QWidget::eventFilter(watched, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailBottomInfoWidget::scheduleFlowLayoutHeightUpdate() {
|
|
||||||
if (m_flowLayoutHeightUpdatePending)
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_flowLayoutHeightUpdatePending = true;
|
|
||||||
QTimer::singleShot(0, this, [this]() {
|
|
||||||
m_flowLayoutHeightUpdatePending = false;
|
|
||||||
updateFlowLayoutHeights();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
int DetailBottomInfoWidget::resolveFlowLayoutWidth(QWidget *widget) const {
|
|
||||||
if (!widget)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
const int maxWidth =
|
|
||||||
widget->maximumWidth() < QWIDGETSIZE_MAX ? widget->maximumWidth() : 0;
|
|
||||||
auto clampWidth = [maxWidth](int width) {
|
|
||||||
if (maxWidth > 0)
|
|
||||||
width = qMin(width, maxWidth);
|
|
||||||
return qMax(0, width);
|
|
||||||
};
|
|
||||||
auto horizontalMargins = [this]() {
|
|
||||||
if (QLayout *mainLayout = this->layout()) {
|
|
||||||
const QMargins margins = mainLayout->contentsMargins();
|
|
||||||
return margins.left() + margins.right();
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
auto contentWidthFrom = [&horizontalMargins](const QWidget *source) {
|
|
||||||
if (!source)
|
|
||||||
return 0;
|
|
||||||
return qMax(0, source->contentsRect().width() - horizontalMargins());
|
|
||||||
};
|
|
||||||
|
|
||||||
QWidget *wrapper = widget->parentWidget();
|
|
||||||
const int wrapperWidth = wrapper ? wrapper->contentsRect().width() : 0;
|
|
||||||
if (isVisible() && wrapper && wrapper->isVisible() && wrapperWidth > 0)
|
|
||||||
return clampWidth(wrapperWidth);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (!isVisible() && maxWidth > 0) {
|
|
||||||
const int parentContentWidth = contentWidthFrom(parentWidget());
|
|
||||||
if (parentContentWidth > 160)
|
|
||||||
return clampWidth(parentContentWidth);
|
|
||||||
return maxWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
int targetWidth = contentWidthFrom(this);
|
|
||||||
if (targetWidth <= 0 && parentWidget())
|
|
||||||
targetWidth = contentWidthFrom(parentWidget());
|
|
||||||
if (targetWidth <= 0)
|
|
||||||
targetWidth = widget->width();
|
|
||||||
if (targetWidth <= 0 && maxWidth > 0)
|
|
||||||
targetWidth = maxWidth;
|
|
||||||
|
|
||||||
return clampWidth(targetWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailBottomInfoWidget::updateFlowLayoutHeight(QWidget *widget,
|
|
||||||
FlowLayout *layout) {
|
|
||||||
if (!widget || !layout)
|
|
||||||
return;
|
|
||||||
|
|
||||||
int targetHeight = 0;
|
|
||||||
if (layout->count() > 0) {
|
|
||||||
const int targetWidth = resolveFlowLayoutWidth(widget);
|
|
||||||
|
|
||||||
if (targetWidth > 0) {
|
|
||||||
layout->invalidate();
|
|
||||||
targetHeight = layout->heightForWidth(targetWidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget->minimumHeight() != targetHeight) {
|
|
||||||
widget->setMinimumHeight(targetHeight);
|
|
||||||
}
|
|
||||||
if (widget->maximumHeight() != targetHeight) {
|
|
||||||
widget->setMaximumHeight(targetHeight);
|
|
||||||
}
|
|
||||||
widget->updateGeometry();
|
|
||||||
|
|
||||||
if (QWidget *wrapper = widget->parentWidget()) {
|
|
||||||
if (wrapper->minimumHeight() != targetHeight) {
|
|
||||||
wrapper->setMinimumHeight(targetHeight);
|
|
||||||
}
|
|
||||||
if (wrapper->maximumHeight() != targetHeight) {
|
|
||||||
wrapper->setMaximumHeight(targetHeight);
|
|
||||||
}
|
|
||||||
wrapper->updateGeometry();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailBottomInfoWidget::updateFlowLayoutHeights() {
|
|
||||||
updateFlowLayoutHeight(m_tagsBottomWidget, m_tagsBottomLayout);
|
|
||||||
updateFlowLayoutHeight(m_studiosWidget, m_studiosLayout);
|
|
||||||
updateFlowLayoutHeight(m_externalLinksWidget, m_externalLinksLayout);
|
|
||||||
updateGeometry();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailBottomInfoWidget::clearLayout(QLayout *layout) {
|
|
||||||
if (!layout)
|
|
||||||
return;
|
|
||||||
QLayoutItem *child;
|
|
||||||
while ((child = layout->takeAt(0)) != nullptr) {
|
|
||||||
if (child->widget()) {
|
|
||||||
child->widget()->hide();
|
|
||||||
child->widget()->setParent(nullptr);
|
|
||||||
child->widget()->deleteLater();
|
|
||||||
} else if (child->layout()) {
|
|
||||||
clearLayout(child->layout());
|
|
||||||
}
|
|
||||||
delete child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QString DetailBottomInfoWidget::formatSize(long long bytes) {
|
|
||||||
if (bytes < 1024)
|
|
||||||
return QString::number(bytes) + " B";
|
|
||||||
if (bytes < 1024 * 1024)
|
|
||||||
return QString::number(bytes / 1024.0, 'f', 1) + " KB";
|
|
||||||
if (bytes < 1024 * 1024 * 1024)
|
|
||||||
return QString::number(bytes / 1048576.0, 'f', 1) + " MB";
|
|
||||||
return QString::number(bytes / 1073741824.0, 'f', 1) + " GB";
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailBottomInfoWidget::addInfoRow(QGridLayout *layout, int &row,
|
|
||||||
const QString &key,
|
|
||||||
const QString &value) {
|
|
||||||
if (value.isEmpty())
|
|
||||||
return;
|
|
||||||
QLabel *kLabel = new QLabel(key);
|
|
||||||
kLabel->setObjectName("stream-info-key");
|
|
||||||
QLabel *vLabel = new QLabel(value);
|
|
||||||
vLabel->setObjectName("stream-info-value");
|
|
||||||
vLabel->setWordWrap(true);
|
|
||||||
layout->addWidget(kLabel, row, 0, Qt::AlignTop | Qt::AlignLeft);
|
|
||||||
layout->addWidget(vLabel, row, 1, Qt::AlignTop | Qt::AlignLeft);
|
|
||||||
row++;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailBottomInfoWidget::setInfo(const MediaItem &item,
|
|
||||||
const QList<MediaSourceInfo> &sources) {
|
|
||||||
clear();
|
|
||||||
|
|
||||||
if (!item.tags.isEmpty()) {
|
|
||||||
m_tagsBottomTitle->show();
|
|
||||||
m_tagsBottomWrapper->show();
|
|
||||||
for (const QString &tag : item.tags) {
|
|
||||||
auto *btn = new DetailTagButton(tag, m_tagsBottomWidget);
|
|
||||||
connect(btn, &QPushButton::clicked, this,
|
|
||||||
[this, tag]() { Q_EMIT filterClicked("Tag", tag); });
|
|
||||||
m_tagsBottomLayout->addWidget(btn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item.studios.isEmpty()) {
|
|
||||||
m_studiosTitle->show();
|
|
||||||
m_studiosWrapper->show();
|
|
||||||
for (const auto &studio : item.studios) {
|
|
||||||
auto *btn = new DetailTagButton(studio.name, m_studiosWidget);
|
|
||||||
connect(btn, &QPushButton::clicked, this,
|
|
||||||
[this, name = studio.name]() { Q_EMIT filterClicked("Studio", name); });
|
|
||||||
m_studiosLayout->addWidget(btn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (!item.externalUrls.isEmpty()) {
|
|
||||||
m_externalLinksTitle->show();
|
|
||||||
m_externalLinksWrapper->show();
|
|
||||||
for (const auto &link : item.externalUrls) {
|
|
||||||
auto *btn = new DetailTagButton(link.name, m_externalLinksWidget);
|
|
||||||
|
|
||||||
connect(btn, &QPushButton::clicked, this,
|
|
||||||
[link]() { QDesktopServices::openUrl(QUrl(link.url)); });
|
|
||||||
m_externalLinksLayout->addWidget(btn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFlowLayoutHeights();
|
|
||||||
scheduleFlowLayoutHeightUpdate();
|
|
||||||
|
|
||||||
if (sources.isEmpty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_mediaInfoTitle->show();
|
|
||||||
m_fileInfoWidget->show();
|
|
||||||
m_mediaInfoGallery->show();
|
|
||||||
|
|
||||||
const auto &source = sources.first();
|
|
||||||
QString actualPath = source.path.isEmpty() ? item.path : source.path;
|
|
||||||
m_filePathLabel->setText(QUrl::fromPercentEncoding(actualPath.toUtf8()));
|
|
||||||
|
|
||||||
QStringList metaParts;
|
|
||||||
QString container =
|
|
||||||
source.container.isEmpty() ? item.container : source.container;
|
|
||||||
if (!container.isEmpty())
|
|
||||||
metaParts << container.toUpper();
|
|
||||||
|
|
||||||
long long size = source.size > 0 ? source.size : item.size;
|
|
||||||
if (size > 0)
|
|
||||||
metaParts << formatSize(size);
|
|
||||||
if (!item.dateCreated.isEmpty())
|
|
||||||
metaParts << tr("Added %1").arg(item.dateCreated);
|
|
||||||
m_fileMetaLabel->setText(metaParts.join(" • "));
|
|
||||||
|
|
||||||
for (const auto &stream : source.mediaStreams) {
|
|
||||||
QWidget *card = new QWidget();
|
|
||||||
card->setObjectName("stream-card");
|
|
||||||
card->setMinimumWidth(280);
|
|
||||||
auto *layout = new QVBoxLayout(card);
|
|
||||||
layout->setContentsMargins(16, 16, 16, 16);
|
|
||||||
layout->setSpacing(8);
|
|
||||||
|
|
||||||
QString typeText = stream.type;
|
|
||||||
if (typeText == "Video")
|
|
||||||
typeText = tr("Video");
|
|
||||||
else if (typeText == "Audio")
|
|
||||||
typeText = tr("Audio");
|
|
||||||
else if (typeText == "Subtitle")
|
|
||||||
typeText = tr("Subtitle");
|
|
||||||
|
|
||||||
QLabel *typeLabel = new QLabel(typeText, card);
|
|
||||||
typeLabel->setObjectName("stream-type");
|
|
||||||
layout->addWidget(typeLabel);
|
|
||||||
|
|
||||||
QGridLayout *grid = new QGridLayout();
|
|
||||||
grid->setContentsMargins(0, 4, 0, 0);
|
|
||||||
grid->setSpacing(8);
|
|
||||||
grid->setColumnMinimumWidth(0, 80);
|
|
||||||
int row = 0;
|
|
||||||
|
|
||||||
addInfoRow(grid, row, tr("Title"), stream.displayTitle);
|
|
||||||
addInfoRow(grid, row, tr("Codec"), stream.codec.toUpper());
|
|
||||||
|
|
||||||
if (stream.type == "Video") {
|
|
||||||
if (!stream.language.isEmpty())
|
|
||||||
addInfoRow(grid, row, tr("Language"), stream.language);
|
|
||||||
if (!stream.codecTag.isEmpty())
|
|
||||||
addInfoRow(grid, row, tr("Codec Tag"), stream.codecTag);
|
|
||||||
if (!stream.profile.isEmpty())
|
|
||||||
addInfoRow(grid, row, tr("Profile"), stream.profile);
|
|
||||||
if (stream.level > 0)
|
|
||||||
addInfoRow(grid, row, tr("Level"), QString::number(stream.level));
|
|
||||||
if (stream.width > 0 && stream.height > 0)
|
|
||||||
addInfoRow(grid, row, tr("Resolution"),
|
|
||||||
QString("%1x%2").arg(stream.width).arg(stream.height));
|
|
||||||
if (!stream.aspectRatio.isEmpty())
|
|
||||||
addInfoRow(grid, row, tr("Aspect Ratio"), stream.aspectRatio);
|
|
||||||
if (stream.isInterlaced)
|
|
||||||
addInfoRow(grid, row, tr("Interlaced"), tr("Yes"));
|
|
||||||
if (stream.realFrameRate > 0)
|
|
||||||
addInfoRow(grid, row, tr("Framerate"),
|
|
||||||
QString::number(stream.realFrameRate, 'f', 2));
|
|
||||||
if (stream.bitRate > 0)
|
|
||||||
addInfoRow(grid, row, tr("Bitrate"),
|
|
||||||
QString("%1 kbps").arg(stream.bitRate / 1000));
|
|
||||||
if (stream.bitDepth > 0)
|
|
||||||
addInfoRow(grid, row, tr("Bit Depth"),
|
|
||||||
QString("%1 bit").arg(stream.bitDepth));
|
|
||||||
if (!stream.pixelFormat.isEmpty())
|
|
||||||
addInfoRow(grid, row, tr("Pixel Format"), stream.pixelFormat);
|
|
||||||
if (stream.refFrames > 0)
|
|
||||||
addInfoRow(grid, row, tr("Ref Frames"),
|
|
||||||
QString::number(stream.refFrames));
|
|
||||||
} else if (stream.type == "Audio") {
|
|
||||||
if (!stream.language.isEmpty())
|
|
||||||
addInfoRow(grid, row, tr("Language"), stream.language);
|
|
||||||
if (!stream.codecTag.isEmpty())
|
|
||||||
addInfoRow(grid, row, tr("Codec Tag"), stream.codecTag);
|
|
||||||
if (!stream.profile.isEmpty())
|
|
||||||
addInfoRow(grid, row, tr("Profile"), stream.profile);
|
|
||||||
if (!stream.channelLayout.isEmpty())
|
|
||||||
addInfoRow(grid, row, tr("Layout"), stream.channelLayout);
|
|
||||||
if (stream.channels > 0)
|
|
||||||
addInfoRow(grid, row, tr("Channels"),
|
|
||||||
QString("%1 ch").arg(stream.channels));
|
|
||||||
if (stream.bitRate > 0)
|
|
||||||
addInfoRow(grid, row, tr("Bitrate"),
|
|
||||||
QString("%1 kbps").arg(stream.bitRate / 1000));
|
|
||||||
if (stream.sampleRate > 0)
|
|
||||||
addInfoRow(grid, row, tr("Sample Rate"),
|
|
||||||
QString("%1 Hz").arg(stream.sampleRate));
|
|
||||||
if (stream.isDefault)
|
|
||||||
addInfoRow(grid, row, tr("Default"), tr("Yes"));
|
|
||||||
} else if (stream.type == "Subtitle") {
|
|
||||||
QString displayLang = stream.displayLanguage.isEmpty()
|
|
||||||
? stream.language
|
|
||||||
: stream.displayLanguage;
|
|
||||||
if (!displayLang.isEmpty())
|
|
||||||
addInfoRow(grid, row, tr("Language"), displayLang);
|
|
||||||
if (stream.index >= 0)
|
|
||||||
addInfoRow(grid, row, tr("Index"), QString::number(stream.index));
|
|
||||||
if (stream.isDefault)
|
|
||||||
addInfoRow(grid, row, tr("Default"), tr("Yes"));
|
|
||||||
if (stream.isForced)
|
|
||||||
addInfoRow(grid, row, tr("Forced"), tr("Yes"));
|
|
||||||
if (stream.isHearingImpaired)
|
|
||||||
addInfoRow(grid, row, tr("Hearing Impaired"), tr("Yes"));
|
|
||||||
if (stream.isExternal)
|
|
||||||
addInfoRow(grid, row, tr("External"), tr("Yes"));
|
|
||||||
if (stream.isTextSubtitleStream)
|
|
||||||
addInfoRow(grid, row, tr("Text Stream"), tr("Yes"));
|
|
||||||
if (stream.supportsExternalStream)
|
|
||||||
addInfoRow(grid, row, tr("Ext. Stream Supported"), tr("Yes"));
|
|
||||||
if (stream.isExternalUrl)
|
|
||||||
addInfoRow(grid, row, tr("External URL"), tr("Yes"));
|
|
||||||
if (!stream.deliveryMethod.isEmpty())
|
|
||||||
addInfoRow(grid, row, tr("Delivery Method"), stream.deliveryMethod);
|
|
||||||
if (!stream.protocol.isEmpty() && stream.protocol != "None")
|
|
||||||
addInfoRow(grid, row, tr("Protocol"), stream.protocol);
|
|
||||||
if (!stream.extendedVideoType.isEmpty() &&
|
|
||||||
stream.extendedVideoType != "None")
|
|
||||||
addInfoRow(grid, row, tr("Ext. Video Type"), stream.extendedVideoType);
|
|
||||||
if (!stream.extendedVideoSubType.isEmpty() &&
|
|
||||||
stream.extendedVideoSubType != "None")
|
|
||||||
addInfoRow(grid, row, tr("Ext. Video SubType"),
|
|
||||||
stream.extendedVideoSubType);
|
|
||||||
if (stream.attachmentSize > 0)
|
|
||||||
addInfoRow(grid, row, tr("Size"), formatSize(stream.attachmentSize));
|
|
||||||
QFileInfo fi(stream.path);
|
|
||||||
if (!stream.path.isEmpty())
|
|
||||||
addInfoRow(grid, row, tr("Path"), fi.completeBaseName());
|
|
||||||
if (!stream.deliveryUrl.isEmpty())
|
|
||||||
addInfoRow(grid, row, tr("Delivery URL"), stream.deliveryUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
layout->addLayout(grid);
|
|
||||||
layout->addStretch();
|
|
||||||
m_mediaInfoGallery->addWidget(card);
|
|
||||||
}
|
|
||||||
m_mediaInfoGallery->adjustHeightToContent();
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
#ifndef DETAILBOTTOMINFOWIDGET_H
|
|
||||||
#define DETAILBOTTOMINFOWIDGET_H
|
|
||||||
|
|
||||||
#include <QWidget>
|
|
||||||
#include "models/media/mediaitem.h"
|
|
||||||
|
|
||||||
class FlowLayout;
|
|
||||||
class HorizontalWidgetGallery;
|
|
||||||
class QLabel;
|
|
||||||
class QVBoxLayout;
|
|
||||||
class QGridLayout;
|
|
||||||
class QEvent;
|
|
||||||
class QResizeEvent;
|
|
||||||
class QShowEvent;
|
|
||||||
|
|
||||||
class DetailBottomInfoWidget : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit DetailBottomInfoWidget(QWidget* parent = nullptr);
|
|
||||||
|
|
||||||
void setInfo(const MediaItem& item, const QList<MediaSourceInfo>& sources);
|
|
||||||
void clear();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void filterClicked(const QString& filterType, const QString& filterValue);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool eventFilter(QObject* watched, QEvent* event) override;
|
|
||||||
void resizeEvent(QResizeEvent* event) override;
|
|
||||||
void showEvent(QShowEvent* event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void clearLayout(QLayout* layout);
|
|
||||||
QString formatSize(long long bytes);
|
|
||||||
void addInfoRow(QGridLayout* layout, int& row, const QString& key, const QString& value);
|
|
||||||
QWidget* wrapMaxWidth(QWidget* child, int maxW);
|
|
||||||
int resolveFlowLayoutWidth(QWidget* widget) const;
|
|
||||||
void updateFlowLayoutHeight(QWidget* widget, FlowLayout* layout);
|
|
||||||
void updateFlowLayoutHeights();
|
|
||||||
void scheduleFlowLayoutHeightUpdate();
|
|
||||||
|
|
||||||
QLabel* m_tagsBottomTitle;
|
|
||||||
QWidget* m_tagsBottomWidget;
|
|
||||||
QWidget* m_tagsBottomWrapper;
|
|
||||||
FlowLayout* m_tagsBottomLayout;
|
|
||||||
|
|
||||||
QLabel* m_studiosTitle;
|
|
||||||
QWidget* m_studiosWidget;
|
|
||||||
QWidget* m_studiosWrapper;
|
|
||||||
FlowLayout* m_studiosLayout;
|
|
||||||
|
|
||||||
|
|
||||||
QLabel* m_externalLinksTitle;
|
|
||||||
QWidget* m_externalLinksWidget;
|
|
||||||
QWidget* m_externalLinksWrapper;
|
|
||||||
FlowLayout* m_externalLinksLayout;
|
|
||||||
|
|
||||||
QLabel* m_mediaInfoTitle;
|
|
||||||
QWidget* m_fileInfoWidget;
|
|
||||||
QLabel* m_filePathLabel;
|
|
||||||
QLabel* m_fileMetaLabel;
|
|
||||||
HorizontalWidgetGallery* m_mediaInfoGallery;
|
|
||||||
|
|
||||||
bool m_flowLayoutHeightUpdatePending = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
#include "detailcontentwidget.h"
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QStyleOption>
|
|
||||||
#include <QLinearGradient>
|
|
||||||
#include <QPropertyAnimation>
|
|
||||||
#include "../tokens/AnimationTokens.h"
|
|
||||||
#include "../utils/motionprefs.h"
|
|
||||||
|
|
||||||
DetailContentWidget::DetailContentWidget(QWidget* parent)
|
|
||||||
: QWidget(parent),
|
|
||||||
m_cachedWidth(-1),
|
|
||||||
m_baseColor(QColor("#FFFFFF")),
|
|
||||||
m_gradientColor(QColor(249, 250, 251))
|
|
||||||
{
|
|
||||||
|
|
||||||
setAttribute(Qt::WA_StyledBackground, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailContentWidget::setBackdrop(const QPixmap& pix) {
|
|
||||||
const bool hadBackdrop = !m_backdrop.isNull();
|
|
||||||
m_backdrop = pix;
|
|
||||||
m_cachedWidth = -1;
|
|
||||||
|
|
||||||
// Fade the hero backdrop in only on first reveal (null -> image), not when a
|
|
||||||
// network refresh replaces an already-shown backdrop (that would flicker).
|
|
||||||
if (!pix.isNull() && !hadBackdrop) {
|
|
||||||
if (MotionPrefs::reduceMotion()) {
|
|
||||||
m_backdropOpacity = 1.0;
|
|
||||||
} else {
|
|
||||||
if (!m_backdropFade) {
|
|
||||||
m_backdropFade = new QPropertyAnimation(this, "backdropOpacity", this);
|
|
||||||
m_backdropFade->setDuration(AnimationTokens::DurationMs::kNormal);
|
|
||||||
m_backdropFade->setEasingCurve(AnimationTokens::Curve::fade());
|
|
||||||
}
|
|
||||||
m_backdropFade->stop();
|
|
||||||
m_backdropFade->setStartValue(0.0);
|
|
||||||
m_backdropFade->setEndValue(1.0);
|
|
||||||
m_backdropFade->start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
qreal DetailContentWidget::backdropOpacity() const {
|
|
||||||
return m_backdropOpacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailContentWidget::setBackdropOpacity(qreal opacity) {
|
|
||||||
m_backdropOpacity = opacity;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
QColor DetailContentWidget::baseColor() const {
|
|
||||||
return m_baseColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailContentWidget::setBaseColor(const QColor& color) {
|
|
||||||
if (m_baseColor != color) {
|
|
||||||
m_baseColor = color;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QColor DetailContentWidget::gradientColor() const {
|
|
||||||
return m_gradientColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailContentWidget::setGradientColor(const QColor& color) {
|
|
||||||
if (m_gradientColor != color) {
|
|
||||||
m_gradientColor = color;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailContentWidget::paintEvent(QPaintEvent* event) {
|
|
||||||
Q_UNUSED(event);
|
|
||||||
|
|
||||||
|
|
||||||
QStyleOption opt;
|
|
||||||
opt.initFrom(this);
|
|
||||||
QPainter painter(this);
|
|
||||||
style()->drawPrimitive(QStyle::PE_Widget, &opt, &painter, this);
|
|
||||||
|
|
||||||
painter.setRenderHint(QPainter::Antialiasing);
|
|
||||||
painter.setRenderHint(QPainter::SmoothPixmapTransform);
|
|
||||||
|
|
||||||
|
|
||||||
painter.fillRect(rect(), m_baseColor);
|
|
||||||
|
|
||||||
if (m_backdrop.isNull()) return;
|
|
||||||
|
|
||||||
int w = width();
|
|
||||||
int imgW = m_backdrop.width();
|
|
||||||
int imgH = m_backdrop.height();
|
|
||||||
if (imgW <= 0) return;
|
|
||||||
|
|
||||||
|
|
||||||
int targetHeight = w * imgH / imgW;
|
|
||||||
|
|
||||||
|
|
||||||
if (w != m_cachedWidth) {
|
|
||||||
m_scaledBackdrop = m_backdrop.scaled(w, targetHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
||||||
m_cachedWidth = w;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
painter.setOpacity(m_backdropOpacity);
|
|
||||||
painter.drawPixmap(0, 0, m_scaledBackdrop);
|
|
||||||
painter.setOpacity(1.0);
|
|
||||||
|
|
||||||
|
|
||||||
QLinearGradient gradient(0, 0, 0, targetHeight);
|
|
||||||
int r = m_gradientColor.red();
|
|
||||||
int g = m_gradientColor.green();
|
|
||||||
int b = m_gradientColor.blue();
|
|
||||||
|
|
||||||
|
|
||||||
gradient.setColorAt(0.0, QColor(r, g, b, 0));
|
|
||||||
gradient.setColorAt(0.15, QColor(r, g, b, 150));
|
|
||||||
gradient.setColorAt(0.4, QColor(r, g, b, 180));
|
|
||||||
gradient.setColorAt(1.0, QColor(r, g, b, 255));
|
|
||||||
|
|
||||||
painter.fillRect(0, 0, w, targetHeight, gradient);
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
#ifndef DETAILCONTENTWIDGET_H
|
|
||||||
#define DETAILCONTENTWIDGET_H
|
|
||||||
|
|
||||||
#include <QWidget>
|
|
||||||
#include <QPixmap>
|
|
||||||
#include <QColor>
|
|
||||||
|
|
||||||
class QPropertyAnimation;
|
|
||||||
|
|
||||||
class DetailContentWidget : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
Q_PROPERTY(QColor baseColor READ baseColor WRITE setBaseColor)
|
|
||||||
Q_PROPERTY(QColor gradientColor READ gradientColor WRITE setGradientColor)
|
|
||||||
Q_PROPERTY(qreal backdropOpacity READ backdropOpacity WRITE setBackdropOpacity)
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit DetailContentWidget(QWidget* parent = nullptr);
|
|
||||||
|
|
||||||
|
|
||||||
void setBackdrop(const QPixmap& pix);
|
|
||||||
|
|
||||||
|
|
||||||
QColor baseColor() const;
|
|
||||||
void setBaseColor(const QColor& color);
|
|
||||||
|
|
||||||
QColor gradientColor() const;
|
|
||||||
void setGradientColor(const QColor& color);
|
|
||||||
|
|
||||||
qreal backdropOpacity() const;
|
|
||||||
void setBackdropOpacity(qreal opacity);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void paintEvent(QPaintEvent* event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
QPixmap m_backdrop;
|
|
||||||
QPixmap m_scaledBackdrop;
|
|
||||||
int m_cachedWidth;
|
|
||||||
|
|
||||||
|
|
||||||
QColor m_baseColor;
|
|
||||||
QColor m_gradientColor;
|
|
||||||
|
|
||||||
qreal m_backdropOpacity = 1.0;
|
|
||||||
QPropertyAnimation* m_backdropFade = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#include "detailtagbutton.h"
|
|
||||||
|
|
||||||
#include <QSizePolicy>
|
|
||||||
|
|
||||||
DetailTagButton::DetailTagButton(const QString &text, QWidget *parent)
|
|
||||||
: QPushButton(text, parent) {
|
|
||||||
setObjectName("detail-genre-tag");
|
|
||||||
setAttribute(Qt::WA_LayoutUsesWidgetRect, true);
|
|
||||||
setCursor(Qt::PointingHandCursor);
|
|
||||||
setFocusPolicy(Qt::NoFocus);
|
|
||||||
setFlat(true);
|
|
||||||
setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed);
|
|
||||||
setFixedHeight(kTagHeight);
|
|
||||||
ensurePolished();
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize DetailTagButton::sizeHint() const {
|
|
||||||
QSize size = QPushButton::sizeHint();
|
|
||||||
size.setHeight(kTagHeight);
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize DetailTagButton::minimumSizeHint() const {
|
|
||||||
QSize size = QPushButton::minimumSizeHint();
|
|
||||||
size.setHeight(kTagHeight);
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
#ifndef DETAILTAGBUTTON_H
|
|
||||||
#define DETAILTAGBUTTON_H
|
|
||||||
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QSize>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
class DetailTagButton : public QPushButton {
|
|
||||||
public:
|
|
||||||
explicit DetailTagButton(const QString &text, QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
QSize sizeHint() const override;
|
|
||||||
QSize minimumSizeHint() const override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
static constexpr int kTagHeight = 28;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
#include "flowlayout.h"
|
|
||||||
#include <QWidget>
|
|
||||||
|
|
||||||
FlowLayout::FlowLayout(QWidget *parent, int margin, int hSpacing, int vSpacing)
|
|
||||||
: QLayout(parent), m_hSpace(hSpacing), m_vSpace(vSpacing)
|
|
||||||
{
|
|
||||||
setContentsMargins(margin, margin, margin, margin);
|
|
||||||
}
|
|
||||||
|
|
||||||
FlowLayout::FlowLayout(int margin, int hSpacing, int vSpacing)
|
|
||||||
: m_hSpace(hSpacing), m_vSpace(vSpacing)
|
|
||||||
{
|
|
||||||
setContentsMargins(margin, margin, margin, margin);
|
|
||||||
}
|
|
||||||
|
|
||||||
FlowLayout::~FlowLayout()
|
|
||||||
{
|
|
||||||
while (!itemList.isEmpty())
|
|
||||||
delete itemList.takeFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FlowLayout::addItem(QLayoutItem *item)
|
|
||||||
{
|
|
||||||
itemList.append(item);
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
int FlowLayout::horizontalSpacing() const
|
|
||||||
{
|
|
||||||
if (m_hSpace >= 0) {
|
|
||||||
return m_hSpace;
|
|
||||||
} else {
|
|
||||||
return smartSpacing(QStyle::PM_LayoutHorizontalSpacing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int FlowLayout::verticalSpacing() const
|
|
||||||
{
|
|
||||||
if (m_vSpace >= 0) {
|
|
||||||
return m_vSpace;
|
|
||||||
} else {
|
|
||||||
return smartSpacing(QStyle::PM_LayoutVerticalSpacing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int FlowLayout::count() const
|
|
||||||
{
|
|
||||||
return itemList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
QLayoutItem *FlowLayout::itemAt(int index) const
|
|
||||||
{
|
|
||||||
return itemList.value(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
QLayoutItem *FlowLayout::takeAt(int index)
|
|
||||||
{
|
|
||||||
if (index >= 0 && index < itemList.size()) {
|
|
||||||
QLayoutItem *item = itemList.takeAt(index);
|
|
||||||
invalidate();
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
Qt::Orientations FlowLayout::expandingDirections() const
|
|
||||||
{
|
|
||||||
return { };
|
|
||||||
}
|
|
||||||
|
|
||||||
bool FlowLayout::hasHeightForWidth() const
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int FlowLayout::heightForWidth(int width) const
|
|
||||||
{
|
|
||||||
int height = doLayout(QRect(0, 0, width, 0), true);
|
|
||||||
return height;
|
|
||||||
}
|
|
||||||
|
|
||||||
void FlowLayout::invalidate()
|
|
||||||
{
|
|
||||||
QLayout::invalidate();
|
|
||||||
updateParentGeometry();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FlowLayout::setGeometry(const QRect &rect)
|
|
||||||
{
|
|
||||||
QLayout::setGeometry(rect);
|
|
||||||
doLayout(rect, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void FlowLayout::setFirstLineTrailingSpacing(int spacing)
|
|
||||||
{
|
|
||||||
const int safeSpacing = qMax(0, spacing);
|
|
||||||
if (m_firstLineTrailingSpacing == safeSpacing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_firstLineTrailingSpacing = safeSpacing;
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
int FlowLayout::firstLineTrailingSpacing() const
|
|
||||||
{
|
|
||||||
return m_firstLineTrailingSpacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize FlowLayout::sizeHint() const
|
|
||||||
{
|
|
||||||
QSize size = minimumSize();
|
|
||||||
int width = -1;
|
|
||||||
|
|
||||||
if (const auto *parentWidget = qobject_cast<const QWidget *>(parent())) {
|
|
||||||
width = parentWidget->contentsRect().width();
|
|
||||||
const QWidget *p = parentWidget->parentWidget();
|
|
||||||
while (width <= 0 && p) {
|
|
||||||
width = p->contentsRect().width();
|
|
||||||
p = p->parentWidget();
|
|
||||||
}
|
|
||||||
} else if (geometry().isValid()) {
|
|
||||||
width = geometry().width();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (width > 0) {
|
|
||||||
size.setWidth(qMax(size.width(), width));
|
|
||||||
size.setHeight(heightForWidth(width));
|
|
||||||
}
|
|
||||||
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize FlowLayout::minimumSize() const
|
|
||||||
{
|
|
||||||
QSize size;
|
|
||||||
for (const QLayoutItem *item : itemList) {
|
|
||||||
if (const QWidget *wid = item->widget(); wid && wid->isHidden()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
size = size.expandedTo(item->minimumSize());
|
|
||||||
}
|
|
||||||
|
|
||||||
const QMargins margins = contentsMargins();
|
|
||||||
size += QSize(margins.left() + margins.right(), margins.top() + margins.bottom());
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
int FlowLayout::doLayout(const QRect &rect, bool testOnly) const
|
|
||||||
{
|
|
||||||
int left, top, right, bottom;
|
|
||||||
getContentsMargins(&left, &top, &right, &bottom);
|
|
||||||
QRect effectiveRect = rect.adjusted(+left, +top, -right, -bottom);
|
|
||||||
int x = effectiveRect.x();
|
|
||||||
int y = effectiveRect.y();
|
|
||||||
int lineHeight = 0;
|
|
||||||
|
|
||||||
for (QLayoutItem *item : itemList) {
|
|
||||||
const QWidget *wid = item->widget();
|
|
||||||
if (wid && wid->isHidden()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int spaceX = horizontalSpacing();
|
|
||||||
if (spaceX == -1) {
|
|
||||||
spaceX = wid ? wid->style()->layoutSpacing(
|
|
||||||
QSizePolicy::PushButton,
|
|
||||||
QSizePolicy::PushButton,
|
|
||||||
Qt::Horizontal)
|
|
||||||
: 0;
|
|
||||||
}
|
|
||||||
int spaceY = verticalSpacing();
|
|
||||||
if (spaceY == -1) {
|
|
||||||
spaceY = wid ? wid->style()->layoutSpacing(
|
|
||||||
QSizePolicy::PushButton,
|
|
||||||
QSizePolicy::PushButton,
|
|
||||||
Qt::Vertical)
|
|
||||||
: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int lineRight = (y == effectiveRect.y())
|
|
||||||
? qMax(effectiveRect.x(),
|
|
||||||
effectiveRect.right() - m_firstLineTrailingSpacing)
|
|
||||||
: effectiveRect.right();
|
|
||||||
int nextX = x + item->sizeHint().width() + spaceX;
|
|
||||||
if (nextX - spaceX > lineRight && lineHeight > 0) {
|
|
||||||
x = effectiveRect.x();
|
|
||||||
y = y + lineHeight + spaceY;
|
|
||||||
nextX = x + item->sizeHint().width() + spaceX;
|
|
||||||
lineHeight = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!testOnly)
|
|
||||||
item->setGeometry(QRect(QPoint(x, y), item->sizeHint()));
|
|
||||||
|
|
||||||
x = nextX;
|
|
||||||
lineHeight = qMax(lineHeight, item->sizeHint().height());
|
|
||||||
}
|
|
||||||
return y + lineHeight - rect.y() + bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
int FlowLayout::smartSpacing(QStyle::PixelMetric pm) const
|
|
||||||
{
|
|
||||||
QObject *parent = this->parent();
|
|
||||||
if (!parent) {
|
|
||||||
return -1;
|
|
||||||
} else if (parent->isWidgetType()) {
|
|
||||||
QWidget *pw = static_cast<QWidget *>(parent);
|
|
||||||
return pw->style()->pixelMetric(pm, nullptr, pw);
|
|
||||||
} else {
|
|
||||||
return static_cast<QLayout *>(parent)->spacing();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FlowLayout::updateParentGeometry() const
|
|
||||||
{
|
|
||||||
auto *parentWidget = qobject_cast<QWidget *>(parent());
|
|
||||||
if (!parentWidget) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
parentWidget->updateGeometry();
|
|
||||||
if (QWidget *outerWidget = parentWidget->parentWidget()) {
|
|
||||||
outerWidget->updateGeometry();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
#ifndef FLOWLAYOUT_H
|
|
||||||
#define FLOWLAYOUT_H
|
|
||||||
|
|
||||||
#include <QLayout>
|
|
||||||
#include <QRect>
|
|
||||||
#include <QStyle>
|
|
||||||
#include <QList>
|
|
||||||
|
|
||||||
class FlowLayout : public QLayout
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit FlowLayout(QWidget *parent, int margin = -1, int hSpacing = -1, int vSpacing = -1);
|
|
||||||
explicit FlowLayout(int margin = -1, int hSpacing = -1, int vSpacing = -1);
|
|
||||||
~FlowLayout();
|
|
||||||
|
|
||||||
void addItem(QLayoutItem *item) override;
|
|
||||||
int horizontalSpacing() const;
|
|
||||||
int verticalSpacing() const;
|
|
||||||
Qt::Orientations expandingDirections() const override;
|
|
||||||
bool hasHeightForWidth() const override;
|
|
||||||
int heightForWidth(int) const override;
|
|
||||||
void invalidate() override;
|
|
||||||
int count() const override;
|
|
||||||
QLayoutItem *itemAt(int index) const override;
|
|
||||||
QSize minimumSize() const override;
|
|
||||||
void setGeometry(const QRect &rect) override;
|
|
||||||
void setFirstLineTrailingSpacing(int spacing);
|
|
||||||
int firstLineTrailingSpacing() const;
|
|
||||||
QSize sizeHint() const override;
|
|
||||||
QLayoutItem *takeAt(int index) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
int doLayout(const QRect &rect, bool testOnly) const;
|
|
||||||
int smartSpacing(QStyle::PixelMetric pm) const;
|
|
||||||
void updateParentGeometry() const;
|
|
||||||
|
|
||||||
QList<QLayoutItem *> itemList;
|
|
||||||
int m_hSpace;
|
|
||||||
int m_vSpace;
|
|
||||||
int m_firstLineTrailingSpacing = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#include "galleryoverlayfilter.h"
|
|
||||||
#include <QListView>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QEvent>
|
|
||||||
#include <QResizeEvent>
|
|
||||||
#include <QScrollBar>
|
|
||||||
|
|
||||||
|
|
||||||
GalleryOverlayFilter::GalleryOverlayFilter(QListView *list, QPushButton *left, QPushButton *right, QObject *parent)
|
|
||||||
: QObject(parent), m_list(list), m_left(left), m_right(right) {}
|
|
||||||
|
|
||||||
bool GalleryOverlayFilter::eventFilter(QObject *obj, QEvent *event) {
|
|
||||||
if (event->type() == QEvent::Resize) {
|
|
||||||
|
|
||||||
int btnWidth = 40;
|
|
||||||
int btnHeight = 80;
|
|
||||||
|
|
||||||
|
|
||||||
int y = m_list->y() + (m_list->height() - btnHeight) / 2 - 20;
|
|
||||||
if (y < 0) y = 0;
|
|
||||||
|
|
||||||
|
|
||||||
m_left->setGeometry(m_list->x() + 10, y, btnWidth, btnHeight);
|
|
||||||
m_right->setGeometry(m_list->x() + m_list->width() - btnWidth - 10, y, btnWidth, btnHeight);
|
|
||||||
}
|
|
||||||
return QObject::eventFilter(obj, event);
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
#include <QObject>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class QListView;
|
|
||||||
class QPushButton;
|
|
||||||
class GalleryOverlayFilter : public QObject {
|
|
||||||
public:
|
|
||||||
GalleryOverlayFilter(QListView* list, QPushButton* left, QPushButton* right, QObject* parent = nullptr);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
|
||||||
private:
|
|
||||||
QListView* m_list;
|
|
||||||
QPushButton* m_left;
|
|
||||||
QPushButton* m_right;
|
|
||||||
};
|
|
||||||
@@ -1,560 +0,0 @@
|
|||||||
#include "horizontallistviewgallery.h"
|
|
||||||
#include "shimmerwidget.h"
|
|
||||||
#include "../utils/textwraputils.h"
|
|
||||||
#include "../views/media/medialistmodel.h"
|
|
||||||
#include <QListView>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QScrollBar>
|
|
||||||
#include <QPropertyAnimation>
|
|
||||||
#include <QEvent>
|
|
||||||
#include "../tokens/AnimationTokens.h"
|
|
||||||
#include "../utils/fadehelper.h"
|
|
||||||
#include "../utils/motionprefs.h"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
// Fade the nav arrows instead of popping; only reacts to desired-visibility
|
|
||||||
// changes (this runs on every mouse move).
|
|
||||||
void setNavButtonVisible(QPushButton* btn, bool visible)
|
|
||||||
{
|
|
||||||
const QVariant last = btn->property("navVisible");
|
|
||||||
if (!last.isNull() && last.toBool() == visible) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
btn->setProperty("navVisible", visible);
|
|
||||||
if (visible) {
|
|
||||||
FadeHelper::fadeIn(btn, AnimationTokens::DurationMs::kQuick);
|
|
||||||
} else {
|
|
||||||
FadeHelper::fadeOut(btn, AnimationTokens::DurationMs::kMicro,
|
|
||||||
AnimationTokens::Curve::exit(), [btn]() { btn->hide(); });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
#include <QWheelEvent>
|
|
||||||
#include <QCursor>
|
|
||||||
#include <QScroller>
|
|
||||||
#include <QScrollerProperties>
|
|
||||||
#include <QStyleOptionViewItem>
|
|
||||||
#include <QTimer>
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QSet>
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
HorizontalListViewGallery::HorizontalListViewGallery(QEmbyCore* core, QWidget* parent)
|
|
||||||
: QWidget(parent), m_core(core), m_hScrollAnim(nullptr), m_hScrollTarget(0), m_cardStyle(MediaCardDelegate::Poster)
|
|
||||||
{
|
|
||||||
setObjectName("horizontal-listview-gallery");
|
|
||||||
setAttribute(Qt::WA_StyledBackground, true);
|
|
||||||
setMouseTracking(true);
|
|
||||||
|
|
||||||
auto* mainLayout = new QVBoxLayout(this);
|
|
||||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
mainLayout->setSpacing(0);
|
|
||||||
|
|
||||||
m_listView = new QListView(this);
|
|
||||||
m_listView->setViewMode(QListView::IconMode);
|
|
||||||
m_listView->setFlow(QListView::LeftToRight);
|
|
||||||
m_listView->setWrapping(false);
|
|
||||||
m_listView->setSpacing(0);
|
|
||||||
m_listView->setMovement(QListView::Static);
|
|
||||||
m_listView->setResizeMode(QListView::Adjust);
|
|
||||||
m_listView->setUniformItemSizes(true);
|
|
||||||
|
|
||||||
m_listView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
||||||
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
||||||
m_listView->setFrameShape(QFrame::NoFrame);
|
|
||||||
m_listView->setMouseTracking(true);
|
|
||||||
m_listView->viewport()->setAttribute(Qt::WA_Hover);
|
|
||||||
m_listView->setStyleSheet("QListView { background: transparent; outline: none; border: none; }");
|
|
||||||
m_listView->setSelectionMode(QAbstractItemView::NoSelection);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
m_listView->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
|
|
||||||
|
|
||||||
|
|
||||||
QScroller::grabGesture(m_listView->viewport(), QScroller::LeftMouseButtonGesture);
|
|
||||||
QScroller* scroller = QScroller::scroller(m_listView->viewport());
|
|
||||||
QScrollerProperties props = scroller->scrollerProperties();
|
|
||||||
|
|
||||||
props.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
|
|
||||||
props.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
|
|
||||||
|
|
||||||
props.setScrollMetric(QScrollerProperties::DragStartDistance, 0.001);
|
|
||||||
scroller->setScrollerProperties(props);
|
|
||||||
|
|
||||||
|
|
||||||
m_hScrollAnim = new QPropertyAnimation(m_listView->horizontalScrollBar(), "value", this);
|
|
||||||
m_hScrollAnim->setEasingCurve(AnimationTokens::Curve::enter());
|
|
||||||
m_hScrollAnim->setDuration(AnimationTokens::DurationMs::kSlow);
|
|
||||||
|
|
||||||
mainLayout->addWidget(m_listView);
|
|
||||||
|
|
||||||
|
|
||||||
m_shimmer = new ShimmerWidget(this);
|
|
||||||
m_shimmer->hide();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
m_listModel = new MediaListModel(400, m_core, this);
|
|
||||||
m_listDelegate = new MediaCardDelegate(MediaCardDelegate::Poster, this);
|
|
||||||
|
|
||||||
m_listView->setModel(m_listModel);
|
|
||||||
m_listView->setItemDelegate(m_listDelegate);
|
|
||||||
|
|
||||||
|
|
||||||
connect(m_listDelegate, &MediaCardDelegate::playRequested, this, &HorizontalListViewGallery::playRequested);
|
|
||||||
connect(m_listDelegate, &MediaCardDelegate::favoriteRequested, this, &HorizontalListViewGallery::favoriteRequested);
|
|
||||||
connect(m_listDelegate, &MediaCardDelegate::moreMenuRequested, this, &HorizontalListViewGallery::moreMenuRequested);
|
|
||||||
|
|
||||||
|
|
||||||
connect(m_listView, &QListView::clicked, this, [this](const QModelIndex& index) {
|
|
||||||
if (m_listModel) {
|
|
||||||
Q_EMIT itemClicked(m_listModel->getItem(index));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
m_btnLeft = new QPushButton("❮", this);
|
|
||||||
m_btnRight = new QPushButton("❯", this);
|
|
||||||
|
|
||||||
QString btnStyle = "QPushButton { background-color: rgba(0,0,0,120); color: white; font-size: 20px; border: none; border-radius: 8px; }"
|
|
||||||
"QPushButton:hover { background-color: rgba(60,60,60,200); }";
|
|
||||||
m_btnLeft->setStyleSheet(btnStyle);
|
|
||||||
m_btnRight->setStyleSheet(btnStyle);
|
|
||||||
|
|
||||||
m_btnLeft->setFixedSize(40, 60);
|
|
||||||
m_btnRight->setFixedSize(40, 60);
|
|
||||||
m_btnLeft->setCursor(Qt::PointingHandCursor);
|
|
||||||
m_btnRight->setCursor(Qt::PointingHandCursor);
|
|
||||||
m_btnLeft->setFocusPolicy(Qt::NoFocus);
|
|
||||||
m_btnRight->setFocusPolicy(Qt::NoFocus);
|
|
||||||
|
|
||||||
m_btnLeft->hide();
|
|
||||||
m_btnRight->hide();
|
|
||||||
|
|
||||||
auto scrollAction = [this](int directionMultiplier) {
|
|
||||||
QScrollBar* bar = m_listView->horizontalScrollBar();
|
|
||||||
int step = this->width() / 2;
|
|
||||||
int targetValue = bar->value() + directionMultiplier * step;
|
|
||||||
targetValue = qBound(0, targetValue, bar->maximum());
|
|
||||||
|
|
||||||
if (MotionPrefs::reduceMotion()) {
|
|
||||||
bar->setValue(targetValue);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto* anim = new QPropertyAnimation(bar, "value", this);
|
|
||||||
anim->setDuration(AnimationTokens::DurationMs::kSlow);
|
|
||||||
anim->setEasingCurve(AnimationTokens::Curve::enter());
|
|
||||||
anim->setStartValue(bar->value());
|
|
||||||
anim->setEndValue(targetValue);
|
|
||||||
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
|
||||||
};
|
|
||||||
|
|
||||||
connect(m_btnLeft, &QPushButton::clicked, [scrollAction]() { scrollAction(-1); });
|
|
||||||
connect(m_btnRight, &QPushButton::clicked, [scrollAction]() { scrollAction(1); });
|
|
||||||
|
|
||||||
connect(m_listView->horizontalScrollBar(), &QScrollBar::valueChanged, this,
|
|
||||||
[this]() {
|
|
||||||
updateButtonsVisibility();
|
|
||||||
updateVisibleImagePriority();
|
|
||||||
});
|
|
||||||
connect(m_listView->horizontalScrollBar(), &QScrollBar::rangeChanged, this, &HorizontalListViewGallery::updateButtonsVisibility);
|
|
||||||
|
|
||||||
m_listView->viewport()->installEventFilter(this);
|
|
||||||
this->installEventFilter(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalListViewGallery::~HorizontalListViewGallery()
|
|
||||||
{
|
|
||||||
if (m_hScrollAnim) {
|
|
||||||
m_hScrollAnim->stop();
|
|
||||||
}
|
|
||||||
if (m_listView && m_listView->viewport()) {
|
|
||||||
m_listView->viewport()->removeEventFilter(this);
|
|
||||||
QScroller::ungrabGesture(m_listView->viewport());
|
|
||||||
}
|
|
||||||
removeEventFilter(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void HorizontalListViewGallery::setItems(const QList<MediaItem>& items)
|
|
||||||
{
|
|
||||||
if (m_listModel) {
|
|
||||||
m_listModel->setItems(items);
|
|
||||||
QTimer::singleShot(0, this,
|
|
||||||
[this]() { updateVisibleImagePriority(); });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_shimmer && m_shimmer->isVisible() && !items.isEmpty()) {
|
|
||||||
// Cross-fade the skeleton out to reveal the now-populated list underneath.
|
|
||||||
FadeHelper::fadeOut(m_shimmer, AnimationTokens::DurationMs::kNormal,
|
|
||||||
AnimationTokens::Curve::fade(), [this]() {
|
|
||||||
if (m_shimmer) {
|
|
||||||
m_shimmer->stopAnimation();
|
|
||||||
m_shimmer->hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void HorizontalListViewGallery::updateItem(const MediaItem& item)
|
|
||||||
{
|
|
||||||
if (m_listModel) {
|
|
||||||
m_listModel->updateItem(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalListViewGallery::prependOrUpdateItem(const MediaItem& item,
|
|
||||||
int maxItems)
|
|
||||||
{
|
|
||||||
if (m_listModel) {
|
|
||||||
m_listModel->prependOrUpdateItem(item, maxItems);
|
|
||||||
QTimer::singleShot(0, this, [this]() {
|
|
||||||
updateVisibleImagePriority();
|
|
||||||
updateButtonsVisibility();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void HorizontalListViewGallery::removeItem(const QString& itemId)
|
|
||||||
{
|
|
||||||
if (m_listModel) {
|
|
||||||
const int previousCount = m_listModel->rowCount();
|
|
||||||
m_listModel->removeItem(itemId);
|
|
||||||
if (m_listView && m_listModel->rowCount() < previousCount) {
|
|
||||||
m_listView->doItemsLayout();
|
|
||||||
m_listView->updateGeometry();
|
|
||||||
m_listView->update();
|
|
||||||
m_listView->viewport()->update();
|
|
||||||
QTimer::singleShot(0, this, [this]() {
|
|
||||||
if (!m_listView) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_listView->doItemsLayout();
|
|
||||||
m_listView->viewport()->update();
|
|
||||||
updateButtonsVisibility();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int HorizontalListViewGallery::itemCount() const
|
|
||||||
{
|
|
||||||
return m_listModel ? m_listModel->rowCount() : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalListViewGallery::clearImageCache()
|
|
||||||
{
|
|
||||||
if (m_listModel) {
|
|
||||||
m_listModel->clearImageCache();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalListViewGallery::setCardStyle(MediaCardDelegate::CardStyle style)
|
|
||||||
{
|
|
||||||
bool styleChanged = (m_cardStyle != style);
|
|
||||||
m_cardStyle = style;
|
|
||||||
if (m_listDelegate) {
|
|
||||||
m_listDelegate->setStyle(style);
|
|
||||||
|
|
||||||
|
|
||||||
if (style == MediaCardDelegate::LibraryTile) {
|
|
||||||
int imgHeight = 160;
|
|
||||||
int imgWidth = qRound(imgHeight * 16.0 / 9.0);
|
|
||||||
int cardWidth = imgWidth + 16;
|
|
||||||
const int hoverExpandH = qRound(imgHeight * 0.035);
|
|
||||||
const int cardHeight =
|
|
||||||
8 + imgHeight + hoverExpandH + 4 + 20 + 1 + 18 + 8;
|
|
||||||
m_listDelegate->setTileSize(QSize(cardWidth, cardHeight));
|
|
||||||
} else if (style == MediaCardDelegate::Poster) {
|
|
||||||
m_listDelegate->setTileSize(QSize(160, 270));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
m_listView->doItemsLayout();
|
|
||||||
m_listView->viewport()->update();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_listModel) {
|
|
||||||
m_listModel->setPreferThumb(style == MediaCardDelegate::LibraryTile || style == MediaCardDelegate::EpisodeList);
|
|
||||||
|
|
||||||
if (styleChanged) {
|
|
||||||
m_listModel->clearImageCache();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateButtonPositions();
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalListViewGallery::setTileSize(const QSize &size)
|
|
||||||
{
|
|
||||||
if (m_listDelegate) {
|
|
||||||
m_listDelegate->setTileSize(size);
|
|
||||||
m_listView->doItemsLayout();
|
|
||||||
m_listView->viewport()->update();
|
|
||||||
}
|
|
||||||
updateButtonPositions();
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalListViewGallery::setTextPixelSizes(int titlePx, int subTitlePx)
|
|
||||||
{
|
|
||||||
if (m_listDelegate) {
|
|
||||||
m_listDelegate->setTextPixelSizes(titlePx, subTitlePx);
|
|
||||||
m_listView->doItemsLayout();
|
|
||||||
m_listView->viewport()->update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalListViewGallery::setContentPadding(int padding)
|
|
||||||
{
|
|
||||||
if (m_listDelegate) {
|
|
||||||
m_listDelegate->setContentPadding(padding);
|
|
||||||
m_listView->doItemsLayout();
|
|
||||||
m_listView->viewport()->update();
|
|
||||||
}
|
|
||||||
updateButtonPositions();
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalListViewGallery::setHoverControls(
|
|
||||||
MediaCardDelegate::HoverControls controls)
|
|
||||||
{
|
|
||||||
if (m_listDelegate) {
|
|
||||||
m_listDelegate->setHoverControls(controls);
|
|
||||||
m_listView->viewport()->update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalListViewGallery::scrollToItemId(const QString &itemId)
|
|
||||||
{
|
|
||||||
const QString targetItemId = itemId.trimmed();
|
|
||||||
if (targetItemId.isEmpty() || !m_listView || !m_listModel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTimer::singleShot(0, this, [this, targetItemId]() {
|
|
||||||
if (!m_listView || !m_listModel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_listView->doItemsLayout();
|
|
||||||
const int rowCount = m_listModel->rowCount();
|
|
||||||
for (int row = 0; row < rowCount; ++row) {
|
|
||||||
const QModelIndex index = m_listModel->index(row, 0);
|
|
||||||
const MediaItem item =
|
|
||||||
index.data(MediaListModel::ItemDataRole).value<MediaItem>();
|
|
||||||
if (item.id != targetItemId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
qDebug() << "[HorizontalListViewGallery] Scroll to item"
|
|
||||||
<< "| itemId=" << targetItemId
|
|
||||||
<< "| row=" << row;
|
|
||||||
m_listView->scrollTo(index, QAbstractItemView::PositionAtCenter);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
qDebug() << "[HorizontalListViewGallery] Scroll target not found"
|
|
||||||
<< "| itemId=" << targetItemId
|
|
||||||
<< "| rowCount=" << rowCount;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalListViewGallery::setHighlightedItemId(const QString &id)
|
|
||||||
{
|
|
||||||
if (m_listDelegate) {
|
|
||||||
m_listDelegate->setHighlightedItemId(id);
|
|
||||||
m_listView->viewport()->update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalListViewGallery::setLoading(bool loading)
|
|
||||||
{
|
|
||||||
if (!m_shimmer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
|
|
||||||
QStyleOptionViewItem opt;
|
|
||||||
const QSize cardSize = m_listDelegate->sizeHint(opt, QModelIndex());
|
|
||||||
m_shimmer->setCardSize(cardSize);
|
|
||||||
m_shimmer->setShowSubtitle(
|
|
||||||
m_cardStyle == MediaCardDelegate::Poster ||
|
|
||||||
m_cardStyle == MediaCardDelegate::Cast);
|
|
||||||
m_shimmer->setGeometry(m_listView->geometry());
|
|
||||||
m_shimmer->raise();
|
|
||||||
// Show + snap opacity back to 1.0 (a prior reveal fade left the effect at 0).
|
|
||||||
FadeHelper::fadeIn(m_shimmer, 0);
|
|
||||||
m_shimmer->startAnimation();
|
|
||||||
} else if (m_shimmer->isVisible()) {
|
|
||||||
FadeHelper::fadeOut(m_shimmer, AnimationTokens::DurationMs::kNormal,
|
|
||||||
AnimationTokens::Curve::fade(), [this]() {
|
|
||||||
if (m_shimmer) {
|
|
||||||
m_shimmer->stopAnimation();
|
|
||||||
m_shimmer->hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
m_shimmer->stopAnimation();
|
|
||||||
m_shimmer->hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalListViewGallery::resizeEvent(QResizeEvent* event)
|
|
||||||
{
|
|
||||||
QWidget::resizeEvent(event);
|
|
||||||
updateButtonPositions();
|
|
||||||
updateButtonsVisibility();
|
|
||||||
updateVisibleImagePriority();
|
|
||||||
|
|
||||||
|
|
||||||
if (m_shimmer && m_shimmer->isVisible()) {
|
|
||||||
m_shimmer->setGeometry(m_listView->geometry());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalListViewGallery::updateButtonPositions()
|
|
||||||
{
|
|
||||||
int currentWidth = this->width();
|
|
||||||
|
|
||||||
|
|
||||||
QStyleOptionViewItem dummyOption;
|
|
||||||
QModelIndex dummyIndex;
|
|
||||||
QSize itemSize = m_listDelegate->sizeHint(dummyOption, dummyIndex);
|
|
||||||
|
|
||||||
int padding = m_listDelegate ? m_listDelegate->contentPadding() : 8;
|
|
||||||
int imgWidth = itemSize.width() - padding * 2;
|
|
||||||
int imgHeight = 0;
|
|
||||||
|
|
||||||
|
|
||||||
if (m_cardStyle == MediaCardDelegate::LibraryTile) {
|
|
||||||
imgHeight = qRound(imgWidth * 9.0 / 16.0);
|
|
||||||
} else {
|
|
||||||
|
|
||||||
imgHeight = qRound(imgWidth * 1.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int imageCenterY = padding + (imgHeight / 2);
|
|
||||||
int btnY = imageCenterY - (m_btnLeft->height() / 2);
|
|
||||||
|
|
||||||
m_btnLeft->move(10, btnY);
|
|
||||||
m_btnRight->move(currentWidth - m_btnRight->width() - 10, btnY);
|
|
||||||
|
|
||||||
m_btnLeft->raise();
|
|
||||||
m_btnRight->raise();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HorizontalListViewGallery::eventFilter(QObject* obj, QEvent* event)
|
|
||||||
{
|
|
||||||
if (event->type() == QEvent::Enter || event->type() == QEvent::MouseMove) {
|
|
||||||
updateButtonsVisibility();
|
|
||||||
} else if (event->type() == QEvent::Leave) {
|
|
||||||
QPoint globalPos = QCursor::pos();
|
|
||||||
if (!this->rect().contains(this->mapFromGlobal(globalPos))) {
|
|
||||||
setNavButtonVisible(m_btnLeft, false);
|
|
||||||
setNavButtonVisible(m_btnRight, false);
|
|
||||||
}
|
|
||||||
} else if (obj == m_listView->viewport() && event->type() == QEvent::ToolTip) {
|
|
||||||
return TextWrapUtils::showWrappedMediaItemToolTip(m_listView, event);
|
|
||||||
} else if (event->type() == QEvent::Wheel) {
|
|
||||||
|
|
||||||
QWheelEvent* wheelEvent = static_cast<QWheelEvent*>(event);
|
|
||||||
if (qAbs(wheelEvent->angleDelta().x()) > qAbs(wheelEvent->angleDelta().y())) {
|
|
||||||
|
|
||||||
if (!m_listView || !m_hScrollAnim || !m_hScrollAnim->targetObject()) {
|
|
||||||
wheelEvent->ignore();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
QScrollBar* hBar = m_listView->horizontalScrollBar();
|
|
||||||
if (hBar) {
|
|
||||||
int currentVal = hBar->value();
|
|
||||||
if (m_hScrollAnim->state() == QAbstractAnimation::Running) {
|
|
||||||
currentVal = m_hScrollTarget;
|
|
||||||
}
|
|
||||||
int step = wheelEvent->angleDelta().x();
|
|
||||||
int newTarget = currentVal - step;
|
|
||||||
newTarget = qBound(hBar->minimum(), newTarget, hBar->maximum());
|
|
||||||
|
|
||||||
if (newTarget != hBar->value()) {
|
|
||||||
m_hScrollTarget = newTarget;
|
|
||||||
m_hScrollAnim->stop();
|
|
||||||
if (MotionPrefs::reduceMotion()) {
|
|
||||||
hBar->setValue(m_hScrollTarget);
|
|
||||||
} else {
|
|
||||||
m_hScrollAnim->setStartValue(hBar->value());
|
|
||||||
m_hScrollAnim->setEndValue(m_hScrollTarget);
|
|
||||||
m_hScrollAnim->start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
|
|
||||||
wheelEvent->ignore();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return QWidget::eventFilter(obj, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalListViewGallery::updateButtonsVisibility()
|
|
||||||
{
|
|
||||||
QPoint globalPos = QCursor::pos();
|
|
||||||
QPoint localPos = this->mapFromGlobal(globalPos);
|
|
||||||
|
|
||||||
if (!this->rect().contains(localPos)) {
|
|
||||||
setNavButtonVisible(m_btnLeft, false);
|
|
||||||
setNavButtonVisible(m_btnRight, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int currentWidth = this->width();
|
|
||||||
QScrollBar* bar = m_listView->horizontalScrollBar();
|
|
||||||
|
|
||||||
bool isLeftHalf = localPos.x() < (currentWidth / 2);
|
|
||||||
|
|
||||||
setNavButtonVisible(m_btnLeft, isLeftHalf && bar->value() > 0);
|
|
||||||
setNavButtonVisible(m_btnRight, !isLeftHalf && bar->value() < bar->maximum());
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalListViewGallery::updateVisibleImagePriority()
|
|
||||||
{
|
|
||||||
if (!m_listView || !m_listModel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QWidget* viewport = m_listView->viewport();
|
|
||||||
if (!viewport) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QStyleOptionViewItem option;
|
|
||||||
const QSize cellSize = m_listDelegate->sizeHint(option, QModelIndex());
|
|
||||||
const int stepX = qMax(1, cellSize.width() / 2);
|
|
||||||
const int stepY = qMax(1, cellSize.height() / 2);
|
|
||||||
|
|
||||||
QSet<int> rowSet;
|
|
||||||
const QRect rect = viewport->rect();
|
|
||||||
for (int y = rect.top(); y <= rect.bottom(); y += stepY) {
|
|
||||||
for (int x = rect.left(); x <= rect.right(); x += stepX) {
|
|
||||||
const QModelIndex idx = m_listView->indexAt(QPoint(x, y));
|
|
||||||
if (idx.isValid()) {
|
|
||||||
rowSet.insert(idx.row());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<int> rows = rowSet.values();
|
|
||||||
std::sort(rows.begin(), rows.end());
|
|
||||||
m_listModel->setPriorityRows(rows);
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
#ifndef HORIZONTALLISTVIEWGALLERY_H
|
|
||||||
#define HORIZONTALLISTVIEWGALLERY_H
|
|
||||||
|
|
||||||
#include <QWidget>
|
|
||||||
#include <QPoint>
|
|
||||||
#include <QList>
|
|
||||||
#include <QSize>
|
|
||||||
#include <models/media/mediaitem.h>
|
|
||||||
|
|
||||||
class ShimmerWidget;
|
|
||||||
#include "../views/media/mediacarddelegate.h"
|
|
||||||
|
|
||||||
class QEmbyCore;
|
|
||||||
class QListView;
|
|
||||||
class QPushButton;
|
|
||||||
class QPropertyAnimation;
|
|
||||||
class MediaListModel;
|
|
||||||
|
|
||||||
class HorizontalListViewGallery : public QWidget
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
|
|
||||||
explicit HorizontalListViewGallery(QEmbyCore* core, QWidget* parent = nullptr);
|
|
||||||
~HorizontalListViewGallery() override;
|
|
||||||
|
|
||||||
|
|
||||||
QListView* listView() const { return m_listView; }
|
|
||||||
|
|
||||||
|
|
||||||
void setItems(const QList<MediaItem>& items);
|
|
||||||
void updateItem(const MediaItem& item);
|
|
||||||
void prependOrUpdateItem(const MediaItem& item, int maxItems = 0);
|
|
||||||
|
|
||||||
|
|
||||||
void removeItem(const QString& itemId);
|
|
||||||
int itemCount() const;
|
|
||||||
void clearImageCache();
|
|
||||||
|
|
||||||
|
|
||||||
void setCardStyle(MediaCardDelegate::CardStyle style);
|
|
||||||
|
|
||||||
|
|
||||||
void setTileSize(const QSize &size);
|
|
||||||
void setTextPixelSizes(int titlePx, int subTitlePx);
|
|
||||||
void setContentPadding(int padding);
|
|
||||||
void setHoverControls(MediaCardDelegate::HoverControls controls);
|
|
||||||
void scrollToItemId(const QString &itemId);
|
|
||||||
|
|
||||||
|
|
||||||
void setHighlightedItemId(const QString &id);
|
|
||||||
|
|
||||||
|
|
||||||
void setLoading(bool loading);
|
|
||||||
|
|
||||||
|
|
||||||
QSize minimumSizeHint() const override {
|
|
||||||
return QSize(1, QWidget::minimumSizeHint().height());
|
|
||||||
}
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
|
|
||||||
void itemClicked(const MediaItem& item);
|
|
||||||
void playRequested(const MediaItem& item);
|
|
||||||
void favoriteRequested(const MediaItem& item);
|
|
||||||
void moreMenuRequested(const MediaItem& item, const QPoint& globalPos);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void resizeEvent(QResizeEvent* event) override;
|
|
||||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void updateButtonsVisibility();
|
|
||||||
void updateButtonPositions();
|
|
||||||
void updateVisibleImagePriority();
|
|
||||||
|
|
||||||
QEmbyCore* m_core;
|
|
||||||
QListView* m_listView;
|
|
||||||
QPushButton* m_btnLeft;
|
|
||||||
QPushButton* m_btnRight;
|
|
||||||
|
|
||||||
|
|
||||||
MediaListModel* m_listModel;
|
|
||||||
MediaCardDelegate* m_listDelegate;
|
|
||||||
|
|
||||||
|
|
||||||
QPropertyAnimation* m_hScrollAnim;
|
|
||||||
int m_hScrollTarget;
|
|
||||||
|
|
||||||
|
|
||||||
MediaCardDelegate::CardStyle m_cardStyle;
|
|
||||||
|
|
||||||
|
|
||||||
ShimmerWidget* m_shimmer = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
#include "horizontalwidgetgallery.h"
|
|
||||||
#include <QScrollArea>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QScrollBar>
|
|
||||||
#include <QPropertyAnimation>
|
|
||||||
#include <QEvent>
|
|
||||||
#include <QWheelEvent>
|
|
||||||
#include "../tokens/AnimationTokens.h"
|
|
||||||
#include "../utils/fadehelper.h"
|
|
||||||
#include "../utils/motionprefs.h"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
// Fade the nav arrows instead of popping; only reacts to desired-visibility
|
|
||||||
// changes (this runs on every mouse move).
|
|
||||||
void setNavButtonVisible(QPushButton* btn, bool visible)
|
|
||||||
{
|
|
||||||
const QVariant last = btn->property("navVisible");
|
|
||||||
if (!last.isNull() && last.toBool() == visible) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
btn->setProperty("navVisible", visible);
|
|
||||||
if (visible) {
|
|
||||||
FadeHelper::fadeIn(btn, AnimationTokens::DurationMs::kQuick);
|
|
||||||
} else {
|
|
||||||
FadeHelper::fadeOut(btn, AnimationTokens::DurationMs::kMicro,
|
|
||||||
AnimationTokens::Curve::exit(), [btn]() { btn->hide(); });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
#include <QTimer>
|
|
||||||
#include <QCursor>
|
|
||||||
#include <QScroller>
|
|
||||||
#include <QScrollerProperties>
|
|
||||||
|
|
||||||
HorizontalWidgetGallery::HorizontalWidgetGallery(QWidget* parent)
|
|
||||||
: QWidget(parent), m_hScrollAnim(nullptr), m_hScrollTarget(0)
|
|
||||||
{
|
|
||||||
setObjectName("horizontal-widget-gallery");
|
|
||||||
setAttribute(Qt::WA_StyledBackground, true);
|
|
||||||
|
|
||||||
setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
|
||||||
this->setMinimumWidth(1);
|
|
||||||
this->setMouseTracking(true);
|
|
||||||
|
|
||||||
auto* mainLayout = new QVBoxLayout(this);
|
|
||||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
mainLayout->setSpacing(0);
|
|
||||||
|
|
||||||
m_scrollArea = new QScrollArea(this);
|
|
||||||
m_scrollArea->setMinimumWidth(1);
|
|
||||||
m_scrollArea->setWidgetResizable(true);
|
|
||||||
m_scrollArea->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
|
||||||
m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
||||||
m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
||||||
m_scrollArea->setFrameShape(QFrame::NoFrame);
|
|
||||||
m_scrollArea->setStyleSheet("QScrollArea { background: transparent; border: none; }");
|
|
||||||
m_scrollArea->viewport()->setObjectName("gallery-viewport");
|
|
||||||
m_scrollArea->viewport()->setStyleSheet("#gallery-viewport { background: transparent; }");
|
|
||||||
m_scrollArea->viewport()->setMouseTracking(true);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
QScroller::grabGesture(m_scrollArea->viewport(), QScroller::LeftMouseButtonGesture);
|
|
||||||
QScroller* scroller = QScroller::scroller(m_scrollArea->viewport());
|
|
||||||
QScrollerProperties props = scroller->scrollerProperties();
|
|
||||||
|
|
||||||
props.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
|
|
||||||
props.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
|
|
||||||
|
|
||||||
props.setScrollMetric(QScrollerProperties::DragStartDistance, 0.001);
|
|
||||||
scroller->setScrollerProperties(props);
|
|
||||||
|
|
||||||
|
|
||||||
m_hScrollAnim = new QPropertyAnimation(m_scrollArea->horizontalScrollBar(), "value", this);
|
|
||||||
m_hScrollAnim->setEasingCurve(AnimationTokens::Curve::enter());
|
|
||||||
m_hScrollAnim->setDuration(AnimationTokens::DurationMs::kSlow);
|
|
||||||
|
|
||||||
m_contentWidget = new QWidget(m_scrollArea);
|
|
||||||
m_contentWidget->setObjectName("gallery-content-widget");
|
|
||||||
m_contentWidget->setStyleSheet("#gallery-content-widget { background: transparent; }");
|
|
||||||
m_contentWidget->setMouseTracking(true);
|
|
||||||
|
|
||||||
m_contentLayout = new QHBoxLayout(m_contentWidget);
|
|
||||||
m_contentLayout->setSizeConstraint(QLayout::SetMinimumSize);
|
|
||||||
m_contentLayout->setContentsMargins(0, 0, 100, 0);
|
|
||||||
m_contentLayout->setSpacing(16);
|
|
||||||
m_contentLayout->setAlignment(Qt::AlignLeft | Qt::AlignTop);
|
|
||||||
|
|
||||||
m_scrollArea->setWidget(m_contentWidget);
|
|
||||||
mainLayout->addWidget(m_scrollArea);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
m_btnLeft = new QPushButton("❮", this);
|
|
||||||
m_btnRight = new QPushButton("❯", this);
|
|
||||||
|
|
||||||
QString btnStyle = "QPushButton { background-color: rgba(0,0,0,120); color: white; font-size: 20px; border: none; border-radius: 8px; }"
|
|
||||||
"QPushButton:hover { background-color: rgba(60,60,60,200); }";
|
|
||||||
m_btnLeft->setStyleSheet(btnStyle);
|
|
||||||
m_btnRight->setStyleSheet(btnStyle);
|
|
||||||
|
|
||||||
m_btnLeft->setFixedSize(40, 60);
|
|
||||||
m_btnRight->setFixedSize(40, 60);
|
|
||||||
m_btnLeft->setCursor(Qt::PointingHandCursor);
|
|
||||||
m_btnRight->setCursor(Qt::PointingHandCursor);
|
|
||||||
m_btnLeft->setFocusPolicy(Qt::NoFocus);
|
|
||||||
m_btnRight->setFocusPolicy(Qt::NoFocus);
|
|
||||||
|
|
||||||
m_btnLeft->hide();
|
|
||||||
m_btnRight->hide();
|
|
||||||
|
|
||||||
auto scrollAction = [this](int directionMultiplier) {
|
|
||||||
QScrollBar* bar = m_scrollArea->horizontalScrollBar();
|
|
||||||
int step = this->width() / 2;
|
|
||||||
int targetValue = bar->value() + directionMultiplier * step;
|
|
||||||
targetValue = qBound(0, targetValue, bar->maximum());
|
|
||||||
|
|
||||||
if (MotionPrefs::reduceMotion()) {
|
|
||||||
bar->setValue(targetValue);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto* anim = new QPropertyAnimation(bar, "value", this);
|
|
||||||
anim->setDuration(AnimationTokens::DurationMs::kSlow);
|
|
||||||
anim->setEasingCurve(AnimationTokens::Curve::enter());
|
|
||||||
anim->setStartValue(bar->value());
|
|
||||||
anim->setEndValue(targetValue);
|
|
||||||
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
|
||||||
};
|
|
||||||
|
|
||||||
connect(m_btnLeft, &QPushButton::clicked, [scrollAction]() { scrollAction(-1); });
|
|
||||||
connect(m_btnRight, &QPushButton::clicked, [scrollAction]() { scrollAction(1); });
|
|
||||||
|
|
||||||
connect(m_scrollArea->horizontalScrollBar(), &QScrollBar::valueChanged, this, &HorizontalWidgetGallery::updateButtonsVisibility);
|
|
||||||
connect(m_scrollArea->horizontalScrollBar(), &QScrollBar::rangeChanged, this, &HorizontalWidgetGallery::updateButtonsVisibility);
|
|
||||||
|
|
||||||
m_scrollArea->viewport()->installEventFilter(this);
|
|
||||||
this->installEventFilter(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalWidgetGallery::addWidget(QWidget* widget)
|
|
||||||
{
|
|
||||||
if (!widget) return;
|
|
||||||
m_contentLayout->addWidget(widget);
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalWidgetGallery::clear()
|
|
||||||
{
|
|
||||||
QLayoutItem *child;
|
|
||||||
while ((child = m_contentLayout->takeAt(0)) != nullptr) {
|
|
||||||
if (child->widget()) {
|
|
||||||
child->widget()->hide();
|
|
||||||
child->widget()->deleteLater();
|
|
||||||
}
|
|
||||||
delete child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalWidgetGallery::setSpacing(int spacing)
|
|
||||||
{
|
|
||||||
m_contentLayout->setSpacing(spacing);
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalWidgetGallery::updateButtonPositions()
|
|
||||||
{
|
|
||||||
int currentWidth = this->width();
|
|
||||||
int btnY = (this->height() - m_btnLeft->height()) / 2;
|
|
||||||
|
|
||||||
m_btnLeft->move(10, btnY);
|
|
||||||
m_btnRight->move(currentWidth - m_btnRight->width() - 10, btnY);
|
|
||||||
|
|
||||||
m_btnLeft->raise();
|
|
||||||
m_btnRight->raise();
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalWidgetGallery::applyContentHeight()
|
|
||||||
{
|
|
||||||
m_contentWidget->adjustSize();
|
|
||||||
const int contentHeight = m_contentWidget->sizeHint().height();
|
|
||||||
const int targetHeight = qMax(0, contentHeight + 16);
|
|
||||||
if (height() != targetHeight ||
|
|
||||||
minimumHeight() != targetHeight ||
|
|
||||||
maximumHeight() != targetHeight) {
|
|
||||||
setFixedHeight(targetHeight);
|
|
||||||
}
|
|
||||||
updateButtonPositions();
|
|
||||||
updateGeometry();
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalWidgetGallery::adjustHeightToContent()
|
|
||||||
{
|
|
||||||
applyContentHeight();
|
|
||||||
QTimer::singleShot(0, this, [this]() { applyContentHeight(); });
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalWidgetGallery::resizeEvent(QResizeEvent* event)
|
|
||||||
{
|
|
||||||
QScrollBar* bar = m_scrollArea->horizontalScrollBar();
|
|
||||||
bool wasAtEnd = (bar->maximum() > 0 && bar->value() >= bar->maximum() - 5);
|
|
||||||
|
|
||||||
QWidget::resizeEvent(event);
|
|
||||||
updateButtonPositions();
|
|
||||||
updateButtonsVisibility();
|
|
||||||
|
|
||||||
if (wasAtEnd) {
|
|
||||||
QTimer::singleShot(0, this, [bar]() {
|
|
||||||
bar->setValue(bar->maximum());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HorizontalWidgetGallery::eventFilter(QObject* obj, QEvent* event)
|
|
||||||
{
|
|
||||||
if (event->type() == QEvent::Enter || event->type() == QEvent::MouseMove) {
|
|
||||||
updateButtonsVisibility();
|
|
||||||
} else if (event->type() == QEvent::Leave) {
|
|
||||||
QPoint globalPos = QCursor::pos();
|
|
||||||
if (!this->rect().contains(this->mapFromGlobal(globalPos))) {
|
|
||||||
setNavButtonVisible(m_btnLeft, false);
|
|
||||||
setNavButtonVisible(m_btnRight, false);
|
|
||||||
}
|
|
||||||
} else if (event->type() == QEvent::Wheel) {
|
|
||||||
|
|
||||||
QWheelEvent* wheelEvent = static_cast<QWheelEvent*>(event);
|
|
||||||
if (qAbs(wheelEvent->angleDelta().x()) > qAbs(wheelEvent->angleDelta().y())) {
|
|
||||||
|
|
||||||
QScrollBar* hBar = m_scrollArea->horizontalScrollBar();
|
|
||||||
if (hBar) {
|
|
||||||
int currentVal = hBar->value();
|
|
||||||
if (m_hScrollAnim->state() == QAbstractAnimation::Running) {
|
|
||||||
currentVal = m_hScrollTarget;
|
|
||||||
}
|
|
||||||
int step = wheelEvent->angleDelta().x();
|
|
||||||
int newTarget = currentVal - step;
|
|
||||||
newTarget = qBound(hBar->minimum(), newTarget, hBar->maximum());
|
|
||||||
|
|
||||||
if (newTarget != hBar->value()) {
|
|
||||||
m_hScrollTarget = newTarget;
|
|
||||||
m_hScrollAnim->stop();
|
|
||||||
if (MotionPrefs::reduceMotion()) {
|
|
||||||
hBar->setValue(m_hScrollTarget);
|
|
||||||
} else {
|
|
||||||
m_hScrollAnim->setStartValue(hBar->value());
|
|
||||||
m_hScrollAnim->setEndValue(m_hScrollTarget);
|
|
||||||
m_hScrollAnim->start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
|
|
||||||
wheelEvent->ignore();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return QWidget::eventFilter(obj, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalWidgetGallery::wheelEvent(QWheelEvent *event) {
|
|
||||||
if (qAbs(event->angleDelta().x()) <= qAbs(event->angleDelta().y())) {
|
|
||||||
event->ignore();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void HorizontalWidgetGallery::updateButtonsVisibility()
|
|
||||||
{
|
|
||||||
QPoint globalPos = QCursor::pos();
|
|
||||||
QPoint localPos = this->mapFromGlobal(globalPos);
|
|
||||||
|
|
||||||
if (!this->rect().contains(localPos)) {
|
|
||||||
setNavButtonVisible(m_btnLeft, false);
|
|
||||||
setNavButtonVisible(m_btnRight, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int currentWidth = this->width();
|
|
||||||
QScrollBar* bar = m_scrollArea->horizontalScrollBar();
|
|
||||||
|
|
||||||
bool isLeftHalf = localPos.x() < (currentWidth / 2);
|
|
||||||
|
|
||||||
setNavButtonVisible(m_btnLeft, isLeftHalf && bar->value() > 0);
|
|
||||||
setNavButtonVisible(m_btnRight, !isLeftHalf && bar->value() < bar->maximum());
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
#ifndef HORIZONTALWIDGETGALLERY_H
|
|
||||||
#define HORIZONTALWIDGETGALLERY_H
|
|
||||||
|
|
||||||
#include <QWidget>
|
|
||||||
|
|
||||||
class QScrollArea;
|
|
||||||
class QHBoxLayout;
|
|
||||||
class QPushButton;
|
|
||||||
class QPropertyAnimation;
|
|
||||||
|
|
||||||
class HorizontalWidgetGallery : public QWidget
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit HorizontalWidgetGallery(QWidget* parent = nullptr);
|
|
||||||
~HorizontalWidgetGallery() override = default;
|
|
||||||
|
|
||||||
|
|
||||||
void addWidget(QWidget* widget);
|
|
||||||
|
|
||||||
|
|
||||||
void clear();
|
|
||||||
|
|
||||||
|
|
||||||
void setSpacing(int spacing);
|
|
||||||
|
|
||||||
|
|
||||||
void adjustHeightToContent();
|
|
||||||
|
|
||||||
|
|
||||||
QSize minimumSizeHint() const override {
|
|
||||||
return QSize(1, QWidget::minimumSizeHint().height());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void resizeEvent(QResizeEvent* event) override;
|
|
||||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
|
||||||
void wheelEvent(QWheelEvent* event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void applyContentHeight();
|
|
||||||
void updateButtonsVisibility();
|
|
||||||
void updateButtonPositions();
|
|
||||||
|
|
||||||
QScrollArea* m_scrollArea;
|
|
||||||
QWidget* m_contentWidget;
|
|
||||||
QHBoxLayout* m_contentLayout;
|
|
||||||
|
|
||||||
QPushButton* m_btnLeft;
|
|
||||||
QPushButton* m_btnRight;
|
|
||||||
|
|
||||||
|
|
||||||
QPropertyAnimation* m_hScrollAnim;
|
|
||||||
int m_hScrollTarget;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#ifndef IDANMAKUSURFACE_H
|
||||||
|
#define IDANMAKUSURFACE_H
|
||||||
|
|
||||||
|
#include <models/danmaku/danmakumodels.h>
|
||||||
|
|
||||||
|
// Shared surface contract for the QML danmaku surface (DanmakuPaintedItem).
|
||||||
|
// PlayerDanmakuController talks only to this interface + MpvController.
|
||||||
|
class IDanmakuSurface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~IDanmakuSurface() = default;
|
||||||
|
|
||||||
|
virtual void setRenderOptions(DanmakuRenderOptions options) = 0;
|
||||||
|
virtual void setComments(QList<DanmakuComment> comments) = 0;
|
||||||
|
virtual void clearDanmaku() = 0;
|
||||||
|
virtual bool hasComments() const = 0;
|
||||||
|
virtual void setDanmakuVisible(bool visible) = 0;
|
||||||
|
virtual void setBottomSubtitleProtected(bool enabled) = 0;
|
||||||
|
virtual void setExternalFrameClockEnabled(bool enabled) = 0;
|
||||||
|
virtual void advanceFrame() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
#include "libraryactionmenu.h"
|
|
||||||
|
|
||||||
#include "../managers/thememanager.h"
|
|
||||||
#include "../utils/popupfade.h"
|
|
||||||
#include <QAction>
|
|
||||||
#include <QFile>
|
|
||||||
#include <QIcon>
|
|
||||||
#include <QVariant>
|
|
||||||
|
|
||||||
LibraryActionMenu::LibraryActionMenu(const VirtualFolder& folder,
|
|
||||||
bool scanActive, QWidget* parent)
|
|
||||||
: QMenu(parent), m_folder(folder), m_scanActive(scanActive)
|
|
||||||
{
|
|
||||||
|
|
||||||
setObjectName("media-action-menu");
|
|
||||||
setWindowFlags(windowFlags() | Qt::FramelessWindowHint |
|
|
||||||
Qt::NoDropShadowWindowHint);
|
|
||||||
setAttribute(Qt::WA_TranslucentBackground);
|
|
||||||
|
|
||||||
PopupFade::attach(this);
|
|
||||||
|
|
||||||
setupMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
LibraryContextMenuRequest LibraryActionMenu::execRequest(const QPoint& globalPos)
|
|
||||||
{
|
|
||||||
return requestFromAction(exec(globalPos));
|
|
||||||
}
|
|
||||||
|
|
||||||
QAction* LibraryActionMenu::addMenuAction(LibraryContextMenuAction action,
|
|
||||||
const QIcon& icon,
|
|
||||||
const QString& text,
|
|
||||||
const QString& stringValue)
|
|
||||||
{
|
|
||||||
auto* menuAction = new QAction(icon, text, this);
|
|
||||||
|
|
||||||
LibraryContextMenuRequest request;
|
|
||||||
request.action = action;
|
|
||||||
request.stringValue = stringValue;
|
|
||||||
menuAction->setData(QVariant::fromValue(request));
|
|
||||||
|
|
||||||
addAction(menuAction);
|
|
||||||
return menuAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
LibraryContextMenuRequest
|
|
||||||
LibraryActionMenu::requestFromAction(const QAction* action)
|
|
||||||
{
|
|
||||||
if (!action) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const QVariant requestData = action->data();
|
|
||||||
if (!requestData.canConvert<LibraryContextMenuRequest>()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return requestData.value<LibraryContextMenuRequest>();
|
|
||||||
}
|
|
||||||
|
|
||||||
void LibraryActionMenu::setupMenu()
|
|
||||||
{
|
|
||||||
const QString themeDir =
|
|
||||||
ThemeManager::instance()->isDarkMode() ? "dark" : "light";
|
|
||||||
auto adaptiveIcon = [&themeDir](const QString& baseName) -> QIcon {
|
|
||||||
const QString themePath = QString(":/svg/%1/%2").arg(themeDir, baseName);
|
|
||||||
if (QFile::exists(themePath)) {
|
|
||||||
return QIcon(themePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ThemeManager::getAdaptiveIcon(
|
|
||||||
QString(":/svg/player/%1").arg(baseName));
|
|
||||||
};
|
|
||||||
|
|
||||||
const bool hasItemId = !m_folder.itemId.trimmed().isEmpty();
|
|
||||||
|
|
||||||
QAction* editImagesAction = addMenuAction(LibraryContextMenuAction::EditImages,
|
|
||||||
adaptiveIcon("edit-images.svg"),
|
|
||||||
tr("Edit Images"));
|
|
||||||
editImagesAction->setEnabled(hasItemId && !m_scanActive);
|
|
||||||
|
|
||||||
QAction* refreshAction = addMenuAction(LibraryContextMenuAction::RefreshMetadata,
|
|
||||||
adaptiveIcon("refresh.svg"),
|
|
||||||
tr("Refresh Metadata"));
|
|
||||||
refreshAction->setEnabled(hasItemId && !m_scanActive);
|
|
||||||
|
|
||||||
QAction* scanAction = addMenuAction(LibraryContextMenuAction::ScanLibraryFiles,
|
|
||||||
adaptiveIcon("scan-library.svg"),
|
|
||||||
tr("Scan Library Files"));
|
|
||||||
scanAction->setEnabled(hasItemId && !m_scanActive);
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#ifndef LIBRARYACTIONMENU_H
|
|
||||||
#define LIBRARYACTIONMENU_H
|
|
||||||
|
|
||||||
#include "librarycontextmenurequest.h"
|
|
||||||
#include <QMenu>
|
|
||||||
#include <models/admin/adminmodels.h>
|
|
||||||
|
|
||||||
class QAction;
|
|
||||||
class QIcon;
|
|
||||||
class QPoint;
|
|
||||||
|
|
||||||
class LibraryActionMenu : public QMenu
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit LibraryActionMenu(const VirtualFolder& folder, bool scanActive,
|
|
||||||
QWidget* parent = nullptr);
|
|
||||||
LibraryContextMenuRequest execRequest(const QPoint& globalPos);
|
|
||||||
|
|
||||||
private:
|
|
||||||
QAction* addMenuAction(LibraryContextMenuAction action, const QIcon& icon,
|
|
||||||
const QString& text,
|
|
||||||
const QString& stringValue = QString());
|
|
||||||
void setupMenu();
|
|
||||||
static LibraryContextMenuRequest requestFromAction(const QAction* action);
|
|
||||||
|
|
||||||
VirtualFolder m_folder;
|
|
||||||
bool m_scanActive = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
#ifndef LIBRARYCONTEXTMENUREQUEST_H
|
|
||||||
#define LIBRARYCONTEXTMENUREQUEST_H
|
|
||||||
|
|
||||||
#include <QMetaType>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
enum class LibraryContextMenuAction {
|
|
||||||
None,
|
|
||||||
EditImages,
|
|
||||||
RefreshMetadata,
|
|
||||||
ScanLibraryFiles
|
|
||||||
};
|
|
||||||
|
|
||||||
struct LibraryContextMenuRequest {
|
|
||||||
LibraryContextMenuAction action = LibraryContextMenuAction::None;
|
|
||||||
QString stringValue;
|
|
||||||
|
|
||||||
bool isValid() const
|
|
||||||
{
|
|
||||||
return action != LibraryContextMenuAction::None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(LibraryContextMenuRequest)
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,419 +0,0 @@
|
|||||||
#include "mediagridwidget.h"
|
|
||||||
#include "shimmerwidget.h"
|
|
||||||
#include "../utils/smoothscrollcontroller.h"
|
|
||||||
#include "../utils/textwraputils.h"
|
|
||||||
#include "../views/media/medialistmodel.h"
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
#include <QListView>
|
|
||||||
#include <QResizeEvent>
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QStyle>
|
|
||||||
#include <QScroller>
|
|
||||||
#include <QScrollerProperties>
|
|
||||||
#include <QWheelEvent>
|
|
||||||
#include <QScrollBar>
|
|
||||||
#include <QSet>
|
|
||||||
#include <QStyleOptionViewItem>
|
|
||||||
#include <algorithm>
|
|
||||||
#include "../tokens/AnimationTokens.h"
|
|
||||||
#include "../utils/fadehelper.h"
|
|
||||||
|
|
||||||
MediaGridWidget::MediaGridWidget(QEmbyCore* core, QWidget* parent)
|
|
||||||
: QWidget(parent), m_core(core), m_basePadding(20), m_currentStyle(MediaCardDelegate::Poster),
|
|
||||||
m_vScrollController(nullptr)
|
|
||||||
{
|
|
||||||
auto* layout = new QVBoxLayout(this);
|
|
||||||
layout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
|
|
||||||
m_listView = new QListView(this);
|
|
||||||
m_listView->setSelectionMode(QAbstractItemView::NoSelection);
|
|
||||||
m_listView->setViewMode(QListView::IconMode);
|
|
||||||
m_listView->setResizeMode(QListView::Adjust);
|
|
||||||
m_listView->setMovement(QListView::Static);
|
|
||||||
m_listView->setSpacing(0);
|
|
||||||
m_listView->setUniformItemSizes(true);
|
|
||||||
m_listView->setWrapping(true);
|
|
||||||
m_listView->setFrameShape(QFrame::NoFrame);
|
|
||||||
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
||||||
m_listView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
|
||||||
m_listView->setMouseTracking(true);
|
|
||||||
|
|
||||||
|
|
||||||
m_listView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
||||||
m_listView->viewport()->setAttribute(Qt::WA_Hover);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
|
||||||
|
|
||||||
|
|
||||||
QScroller::grabGesture(m_listView->viewport(), QScroller::LeftMouseButtonGesture);
|
|
||||||
QScroller* scroller = QScroller::scroller(m_listView->viewport());
|
|
||||||
QScrollerProperties props = scroller->scrollerProperties();
|
|
||||||
|
|
||||||
props.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
|
|
||||||
props.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
|
|
||||||
|
|
||||||
props.setScrollMetric(QScrollerProperties::DragStartDistance, 0.001);
|
|
||||||
scroller->setScrollerProperties(props);
|
|
||||||
|
|
||||||
m_vScrollController =
|
|
||||||
new SmoothScrollController(m_listView->verticalScrollBar(), this);
|
|
||||||
m_vScrollController->setDuration(AnimationTokens::DurationMs::kFast);
|
|
||||||
|
|
||||||
|
|
||||||
m_listView->viewport()->installEventFilter(this);
|
|
||||||
|
|
||||||
m_listModel = new MediaListModel(400, m_core, this);
|
|
||||||
m_listDelegate = new MediaCardDelegate(m_currentStyle, this);
|
|
||||||
|
|
||||||
m_listView->setModel(m_listModel);
|
|
||||||
m_listView->setItemDelegate(m_listDelegate);
|
|
||||||
layout->addWidget(m_listView);
|
|
||||||
|
|
||||||
|
|
||||||
m_shimmer = new ShimmerWidget(this);
|
|
||||||
m_shimmer->hide();
|
|
||||||
|
|
||||||
connect(m_listView, &QListView::clicked, this, [this](const QModelIndex& index) {
|
|
||||||
Q_EMIT itemClicked(m_listModel->getItem(index));
|
|
||||||
});
|
|
||||||
connect(m_listView->verticalScrollBar(), &QScrollBar::valueChanged, this,
|
|
||||||
[this](int) {
|
|
||||||
notifyLoadMoreIfNeeded();
|
|
||||||
updateVisibleImagePriority();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
connect(m_listDelegate, &MediaCardDelegate::playRequested, this, &MediaGridWidget::playRequested);
|
|
||||||
connect(m_listDelegate, &MediaCardDelegate::favoriteRequested, this, &MediaGridWidget::favoriteRequested);
|
|
||||||
connect(m_listDelegate, &MediaCardDelegate::moreMenuRequested, this, &MediaGridWidget::moreMenuRequested);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void MediaGridWidget::setBasePadding(int padding) {
|
|
||||||
m_basePadding = padding;
|
|
||||||
adjustGrid();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MediaGridWidget::setCardStyle(MediaCardDelegate::CardStyle style) {
|
|
||||||
if (m_currentStyle == style) return;
|
|
||||||
m_currentStyle = style;
|
|
||||||
|
|
||||||
|
|
||||||
m_listDelegate->setStyle(style);
|
|
||||||
const bool preferThumb =
|
|
||||||
style == MediaCardDelegate::LibraryTile ||
|
|
||||||
style == MediaCardDelegate::EpisodeList;
|
|
||||||
m_listModel->setPreferThumb(preferThumb);
|
|
||||||
m_listModel->clearImageCache();
|
|
||||||
|
|
||||||
|
|
||||||
if (style == MediaCardDelegate::EpisodeList) {
|
|
||||||
m_listView->setViewMode(QListView::ListMode);
|
|
||||||
m_listView->setFlow(QListView::TopToBottom);
|
|
||||||
m_listView->setWrapping(false);
|
|
||||||
m_listView->setSpacing(5);
|
|
||||||
} else {
|
|
||||||
m_listView->setViewMode(QListView::IconMode);
|
|
||||||
m_listView->setFlow(QListView::LeftToRight);
|
|
||||||
m_listView->setWrapping(true);
|
|
||||||
m_listView->setSpacing(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustGrid();
|
|
||||||
updateVisibleImagePriority();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MediaGridWidget::setLoading(bool loading)
|
|
||||||
{
|
|
||||||
if (!m_shimmer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
|
|
||||||
QStyleOptionViewItem opt;
|
|
||||||
const QSize cardSize = m_listDelegate->sizeHint(opt, QModelIndex());
|
|
||||||
m_shimmer->setCardSize(cardSize);
|
|
||||||
m_shimmer->setShowSubtitle(false);
|
|
||||||
m_shimmer->setGeometry(m_listView->geometry());
|
|
||||||
m_shimmer->raise();
|
|
||||||
// Show + snap opacity back to 1.0 (a prior reveal fade left the effect at 0).
|
|
||||||
FadeHelper::fadeIn(m_shimmer, 0);
|
|
||||||
m_shimmer->startAnimation();
|
|
||||||
} else if (m_shimmer->isVisible()) {
|
|
||||||
FadeHelper::fadeOut(m_shimmer, AnimationTokens::DurationMs::kNormal,
|
|
||||||
AnimationTokens::Curve::fade(), [this]() {
|
|
||||||
if (m_shimmer) {
|
|
||||||
m_shimmer->stopAnimation();
|
|
||||||
m_shimmer->hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
m_shimmer->stopAnimation();
|
|
||||||
m_shimmer->hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MediaGridWidget::setItems(const QList<MediaItem>& items) {
|
|
||||||
m_listModel->setItems(items);
|
|
||||||
adjustGrid();
|
|
||||||
|
|
||||||
|
|
||||||
if (m_shimmer && m_shimmer->isVisible() && !items.isEmpty()) {
|
|
||||||
// Cross-fade the skeleton out to reveal the now-populated grid underneath.
|
|
||||||
FadeHelper::fadeOut(m_shimmer, AnimationTokens::DurationMs::kNormal,
|
|
||||||
AnimationTokens::Curve::fade(), [this]() {
|
|
||||||
if (m_shimmer) {
|
|
||||||
m_shimmer->stopAnimation();
|
|
||||||
m_shimmer->hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!items.isEmpty()) {
|
|
||||||
QMetaObject::invokeMethod(
|
|
||||||
this,
|
|
||||||
[this]() {
|
|
||||||
notifyLoadMoreIfNeeded();
|
|
||||||
updateVisibleImagePriority();
|
|
||||||
},
|
|
||||||
Qt::QueuedConnection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void MediaGridWidget::updateItem(const MediaItem& item) {
|
|
||||||
if (m_listModel) {
|
|
||||||
m_listModel->updateItem(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MediaGridWidget::prependOrUpdateItem(const MediaItem& item, int maxItems) {
|
|
||||||
if (m_listModel) {
|
|
||||||
m_listModel->prependOrUpdateItem(item, maxItems);
|
|
||||||
QMetaObject::invokeMethod(
|
|
||||||
this,
|
|
||||||
[this]() {
|
|
||||||
notifyLoadMoreIfNeeded();
|
|
||||||
updateVisibleImagePriority();
|
|
||||||
},
|
|
||||||
Qt::QueuedConnection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void MediaGridWidget::removeItem(const QString& itemId) {
|
|
||||||
if (m_listModel) {
|
|
||||||
m_listModel->removeItem(itemId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int MediaGridWidget::itemCount() const {
|
|
||||||
return m_listModel ? m_listModel->rowCount() : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int MediaGridWidget::saveScrollPosition() const {
|
|
||||||
if (m_listView && m_listView->verticalScrollBar())
|
|
||||||
return m_listView->verticalScrollBar()->value();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void MediaGridWidget::restoreScrollPosition(int pos) {
|
|
||||||
if (m_listView && m_listView->verticalScrollBar()) {
|
|
||||||
if (m_vScrollController) {
|
|
||||||
m_vScrollController->scrollTo(pos, false);
|
|
||||||
} else {
|
|
||||||
m_listView->verticalScrollBar()->setValue(pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MediaGridWidget::resizeEvent(QResizeEvent *event) {
|
|
||||||
QWidget::resizeEvent(event);
|
|
||||||
adjustGrid();
|
|
||||||
|
|
||||||
if (m_shimmer && m_shimmer->isVisible()) {
|
|
||||||
m_shimmer->setGeometry(m_listView->geometry());
|
|
||||||
}
|
|
||||||
notifyLoadMoreIfNeeded();
|
|
||||||
updateVisibleImagePriority();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MediaGridWidget::eventFilter(QObject* obj, QEvent* event) {
|
|
||||||
if (obj == m_listView->viewport() && event->type() == QEvent::ToolTip) {
|
|
||||||
return TextWrapUtils::showWrappedMediaItemToolTip(m_listView, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (event->type() == QEvent::Wheel && obj == m_listView->viewport()) {
|
|
||||||
QWheelEvent* we = static_cast<QWheelEvent*>(event);
|
|
||||||
if (qAbs(we->angleDelta().y()) >= qAbs(we->angleDelta().x())) {
|
|
||||||
if (m_vScrollController) {
|
|
||||||
m_vScrollController->scrollByWheelEvent(we, Qt::Vertical);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return QWidget::eventFilter(obj, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MediaGridWidget::notifyLoadMoreIfNeeded()
|
|
||||||
{
|
|
||||||
if (!m_listView || !m_listModel || m_listModel->rowCount() <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QScrollBar* vBar = m_listView->verticalScrollBar();
|
|
||||||
if (!vBar) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr int kLoadMoreThreshold = 160;
|
|
||||||
const int remaining = vBar->maximum() - vBar->value();
|
|
||||||
if (vBar->maximum() <= 0 || remaining <= kLoadMoreThreshold) {
|
|
||||||
Q_EMIT loadMoreRequested();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MediaGridWidget::updateVisibleImagePriority()
|
|
||||||
{
|
|
||||||
if (!m_listView || !m_listModel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QWidget* viewport = m_listView->viewport();
|
|
||||||
if (!viewport) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QStyleOptionViewItem option;
|
|
||||||
const QSize cellSize = m_listDelegate->sizeHint(option, QModelIndex());
|
|
||||||
const int stepX = qMax(1, cellSize.width() / 2);
|
|
||||||
const int stepY = qMax(1, cellSize.height() / 2);
|
|
||||||
|
|
||||||
QSet<int> rowSet;
|
|
||||||
const QRect rect = viewport->rect();
|
|
||||||
for (int y = rect.top(); y <= rect.bottom(); y += stepY) {
|
|
||||||
for (int x = rect.left(); x <= rect.right(); x += stepX) {
|
|
||||||
const QModelIndex idx = m_listView->indexAt(QPoint(x, y));
|
|
||||||
if (idx.isValid()) {
|
|
||||||
rowSet.insert(idx.row());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<int> rows = rowSet.values();
|
|
||||||
std::sort(rows.begin(), rows.end());
|
|
||||||
m_listModel->setPriorityRows(rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MediaGridWidget::adjustGrid() {
|
|
||||||
if (!m_listView || !m_listModel) return;
|
|
||||||
|
|
||||||
int scrollBarWidth = qApp->style()->pixelMetric(QStyle::PM_ScrollBarExtent);
|
|
||||||
int availableWidth = this->width() - scrollBarWidth;
|
|
||||||
if (availableWidth < 100) availableWidth = 100;
|
|
||||||
|
|
||||||
|
|
||||||
if (m_currentStyle == MediaCardDelegate::EpisodeList) {
|
|
||||||
int padding = m_basePadding;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
m_listView->setStyleSheet(QString("QListView { background: transparent; border: none; outline: none; padding-left: %1px; padding-right: 0px; }").arg(padding));
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
int cellWidth = availableWidth - padding - padding;
|
|
||||||
if (cellWidth < 100) cellWidth = 100;
|
|
||||||
|
|
||||||
m_listDelegate->setTileSize(QSize(cellWidth, 160));
|
|
||||||
} else {
|
|
||||||
availableWidth -= (m_basePadding * 2);
|
|
||||||
int defaultCellWidth = 150;
|
|
||||||
if (m_currentStyle == MediaCardDelegate::LibraryTile) {
|
|
||||||
defaultCellWidth = 250;
|
|
||||||
} else if (m_currentStyle == MediaCardDelegate::Cast) {
|
|
||||||
defaultCellWidth = 140;
|
|
||||||
}
|
|
||||||
|
|
||||||
int tolerance = 5;
|
|
||||||
int cols = (availableWidth + tolerance) / defaultCellWidth;
|
|
||||||
if (cols < 1) cols = 1;
|
|
||||||
|
|
||||||
int cellWidth = availableWidth / cols;
|
|
||||||
int remainder = availableWidth - (cols * cellWidth);
|
|
||||||
int leftPad = m_basePadding + remainder / 2;
|
|
||||||
|
|
||||||
m_listView->setStyleSheet(QString("QListView { background: transparent; border: none; outline: none; padding-left: %1px; padding-right: 0px; }").arg(leftPad));
|
|
||||||
|
|
||||||
int imgWidth = cellWidth - 16;
|
|
||||||
int imgHeight = imgWidth;
|
|
||||||
|
|
||||||
if (m_currentStyle == MediaCardDelegate::Poster || m_currentStyle == MediaCardDelegate::Cast) {
|
|
||||||
imgHeight = qRound(imgWidth * 1.5);
|
|
||||||
} else if (m_currentStyle == MediaCardDelegate::LibraryTile) {
|
|
||||||
imgHeight = qRound(imgWidth * 9.0 / 16.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
int cellHeight = imgHeight + 60;
|
|
||||||
m_listDelegate->setTileSize(QSize(cellWidth, cellHeight));
|
|
||||||
}
|
|
||||||
|
|
||||||
m_listView->doItemsLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
#ifndef MEDIAGRIDWIDGET_H
|
|
||||||
#define MEDIAGRIDWIDGET_H
|
|
||||||
|
|
||||||
#include <QWidget>
|
|
||||||
#include <QPoint>
|
|
||||||
#include <models/media/mediaitem.h>
|
|
||||||
#include "../views/media/mediacarddelegate.h"
|
|
||||||
|
|
||||||
class QEmbyCore;
|
|
||||||
class QListView;
|
|
||||||
class MediaListModel;
|
|
||||||
class ShimmerWidget;
|
|
||||||
class SmoothScrollController;
|
|
||||||
|
|
||||||
class MediaGridWidget : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit MediaGridWidget(QEmbyCore* core, QWidget* parent = nullptr);
|
|
||||||
void setItems(const QList<MediaItem>& items);
|
|
||||||
void setBasePadding(int padding);
|
|
||||||
|
|
||||||
|
|
||||||
void setCardStyle(MediaCardDelegate::CardStyle style);
|
|
||||||
|
|
||||||
|
|
||||||
void setLoading(bool loading);
|
|
||||||
|
|
||||||
|
|
||||||
void updateItem(const MediaItem& item);
|
|
||||||
void prependOrUpdateItem(const MediaItem& item, int maxItems = 0);
|
|
||||||
|
|
||||||
|
|
||||||
void removeItem(const QString& itemId);
|
|
||||||
|
|
||||||
|
|
||||||
int itemCount() const;
|
|
||||||
|
|
||||||
|
|
||||||
int saveScrollPosition() const;
|
|
||||||
void restoreScrollPosition(int pos);
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void itemClicked(const MediaItem& item);
|
|
||||||
void loadMoreRequested();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void playRequested(const MediaItem& item);
|
|
||||||
void favoriteRequested(const MediaItem& item);
|
|
||||||
void moreMenuRequested(const MediaItem& item, const QPoint& globalPos);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void resizeEvent(QResizeEvent* event) override;
|
|
||||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void adjustGrid();
|
|
||||||
void notifyLoadMoreIfNeeded();
|
|
||||||
void updateVisibleImagePriority();
|
|
||||||
|
|
||||||
int m_basePadding;
|
|
||||||
MediaCardDelegate::CardStyle m_currentStyle;
|
|
||||||
|
|
||||||
QEmbyCore* m_core;
|
|
||||||
QListView* m_listView;
|
|
||||||
MediaListModel* m_listModel;
|
|
||||||
MediaCardDelegate* m_listDelegate;
|
|
||||||
|
|
||||||
SmoothScrollController* m_vScrollController;
|
|
||||||
|
|
||||||
|
|
||||||
ShimmerWidget* m_shimmer = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
#include "mediasectionwidget.h"
|
|
||||||
#include "horizontallistviewgallery.h"
|
|
||||||
#include <qembycore.h>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QPointer>
|
|
||||||
#include <QListView>
|
|
||||||
|
|
||||||
MediaSectionWidget::MediaSectionWidget(const QString& title, QEmbyCore* core, QWidget* parent)
|
|
||||||
: QWidget(parent), m_core(core)
|
|
||||||
{
|
|
||||||
setObjectName("media-section");
|
|
||||||
setStyleSheet("QWidget#media-section { background: transparent; }");
|
|
||||||
|
|
||||||
auto *layout = new QVBoxLayout(this);
|
|
||||||
layout->setContentsMargins(40, 20, 40, 0);
|
|
||||||
layout->setSpacing(0);
|
|
||||||
|
|
||||||
|
|
||||||
auto *headerContainer = new QWidget(this);
|
|
||||||
headerContainer->setObjectName("section-header");
|
|
||||||
headerContainer->setStyleSheet("#section-header { background: transparent; }");
|
|
||||||
m_headerLayout = new QHBoxLayout(headerContainer);
|
|
||||||
m_headerLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
m_headerLayout->setSpacing(12);
|
|
||||||
|
|
||||||
m_titleLabel = new QLabel(title, headerContainer);
|
|
||||||
m_titleLabel->setObjectName("detail-section-title");
|
|
||||||
m_headerLayout->addWidget(m_titleLabel);
|
|
||||||
m_headerLayout->addStretch();
|
|
||||||
|
|
||||||
m_gallery = new HorizontalListViewGallery(core, this);
|
|
||||||
m_gallery->listView()->setProperty("isHorizontalListView", true);
|
|
||||||
|
|
||||||
|
|
||||||
connect(m_gallery, &HorizontalListViewGallery::itemClicked, this, &MediaSectionWidget::itemClicked);
|
|
||||||
connect(m_gallery, &HorizontalListViewGallery::playRequested, this, &MediaSectionWidget::playRequested);
|
|
||||||
connect(m_gallery, &HorizontalListViewGallery::favoriteRequested, this, &MediaSectionWidget::favoriteRequested);
|
|
||||||
connect(m_gallery, &HorizontalListViewGallery::moreMenuRequested, this, &MediaSectionWidget::moreMenuRequested);
|
|
||||||
|
|
||||||
layout->addWidget(headerContainer);
|
|
||||||
layout->addWidget(m_gallery);
|
|
||||||
|
|
||||||
|
|
||||||
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
|
|
||||||
|
|
||||||
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalListViewGallery* MediaSectionWidget::gallery() const {
|
|
||||||
return m_gallery;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MediaSectionWidget::setTitle(const QString& title) {
|
|
||||||
m_titleLabel->setText(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MediaSectionWidget::setHeaderWidget(QWidget* widget) {
|
|
||||||
|
|
||||||
if (m_headerWidget && m_headerWidget != widget) {
|
|
||||||
m_headerWidget->hide();
|
|
||||||
m_headerWidget = nullptr;
|
|
||||||
}
|
|
||||||
if (widget) {
|
|
||||||
m_headerWidget = widget;
|
|
||||||
|
|
||||||
if (m_headerLayout->indexOf(m_headerWidget) < 0) {
|
|
||||||
m_headerLayout->insertWidget(1, m_headerWidget);
|
|
||||||
}
|
|
||||||
m_headerWidget->show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MediaSectionWidget::setCardStyle(MediaCardDelegate::CardStyle style) {
|
|
||||||
m_gallery->setCardStyle(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MediaSectionWidget::setGalleryHeight(int height) {
|
|
||||||
m_gallery->setFixedHeight(height);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MediaSectionWidget::setTileSize(const QSize &size) {
|
|
||||||
m_gallery->setTileSize(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MediaSectionWidget::clear() {
|
|
||||||
m_gallery->setItems(QList<MediaItem>());
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MediaSectionWidget::setItems(const QList<MediaItem>& items) {
|
|
||||||
if (items.isEmpty()) {
|
|
||||||
hide();
|
|
||||||
} else {
|
|
||||||
m_gallery->setItems(items);
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MediaSectionWidget::updateItem(const MediaItem& item) {
|
|
||||||
m_gallery->updateItem(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
QCoro::Task<void> MediaSectionWidget::loadAsync(FetchFunction fetcher) {
|
|
||||||
if (!fetcher) co_return;
|
|
||||||
|
|
||||||
|
|
||||||
QPointer<MediaSectionWidget> safeThis(this);
|
|
||||||
|
|
||||||
try {
|
|
||||||
QList<MediaItem> items = co_await fetcher();
|
|
||||||
|
|
||||||
if (safeThis) {
|
|
||||||
safeThis->setItems(items);
|
|
||||||
Q_EMIT safeThis->dataLoaded(items.size());
|
|
||||||
}
|
|
||||||
} catch(...) {
|
|
||||||
|
|
||||||
if (safeThis) {
|
|
||||||
safeThis->hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
#ifndef MEDIASECTIONWIDGET_H
|
|
||||||
#define MEDIASECTIONWIDGET_H
|
|
||||||
|
|
||||||
#include <QWidget>
|
|
||||||
#include <functional>
|
|
||||||
#include <qcorotask.h>
|
|
||||||
#include "models/media/mediaitem.h"
|
|
||||||
#include "../views/media/mediacarddelegate.h"
|
|
||||||
|
|
||||||
class QEmbyCore;
|
|
||||||
class HorizontalListViewGallery;
|
|
||||||
class QLabel;
|
|
||||||
class QHBoxLayout;
|
|
||||||
|
|
||||||
class MediaSectionWidget : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit MediaSectionWidget(const QString& title, QEmbyCore* core, QWidget* parent = nullptr);
|
|
||||||
|
|
||||||
|
|
||||||
HorizontalListViewGallery* gallery() const;
|
|
||||||
|
|
||||||
|
|
||||||
void setTitle(const QString& title);
|
|
||||||
void setCardStyle(MediaCardDelegate::CardStyle style);
|
|
||||||
void setGalleryHeight(int height);
|
|
||||||
void setTileSize(const QSize &size);
|
|
||||||
|
|
||||||
|
|
||||||
void setHeaderWidget(QWidget* widget);
|
|
||||||
|
|
||||||
|
|
||||||
void clear();
|
|
||||||
void setItems(const QList<MediaItem>& items);
|
|
||||||
void updateItem(const MediaItem& item);
|
|
||||||
|
|
||||||
|
|
||||||
using FetchFunction = std::function<QCoro::Task<QList<MediaItem>>()>;
|
|
||||||
QCoro::Task<void> loadAsync(FetchFunction fetcher);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
|
|
||||||
void itemClicked(const MediaItem& item);
|
|
||||||
void playRequested(const MediaItem& item);
|
|
||||||
void favoriteRequested(const MediaItem& item);
|
|
||||||
void moreMenuRequested(const MediaItem& item, const QPoint& pos);
|
|
||||||
|
|
||||||
|
|
||||||
void dataLoaded(int itemCount);
|
|
||||||
|
|
||||||
private:
|
|
||||||
QEmbyCore* m_core;
|
|
||||||
QLabel* m_titleLabel;
|
|
||||||
QHBoxLayout* m_headerLayout;
|
|
||||||
QWidget* m_headerWidget = nullptr;
|
|
||||||
HorizontalListViewGallery* m_gallery;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
#include "modernmenubutton.h"
|
|
||||||
#include "../utils/popupfade.h"
|
|
||||||
#include <QAction>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QStyle>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
#include <QWidget>
|
|
||||||
#include <QWidgetAction>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
QWidget *createStreamMenuItemWidget(const QString &firstLineText,
|
|
||||||
const QString &secondLine) {
|
|
||||||
auto *host = new QWidget();
|
|
||||||
host->setObjectName("stream-menu-item-host");
|
|
||||||
host->setAttribute(Qt::WA_StyledBackground, true);
|
|
||||||
|
|
||||||
auto *hostLayout = new QVBoxLayout(host);
|
|
||||||
|
|
||||||
hostLayout->setContentsMargins(0, 1, 0, 1);
|
|
||||||
hostLayout->setSpacing(0);
|
|
||||||
|
|
||||||
auto *container = new QWidget(host);
|
|
||||||
container->setObjectName("stream-menu-item");
|
|
||||||
container->setAttribute(Qt::WA_StyledBackground, true);
|
|
||||||
|
|
||||||
auto *layout = new QVBoxLayout(container);
|
|
||||||
layout->setContentsMargins(14, 6, 28, 6);
|
|
||||||
layout->setSpacing(secondLine.isEmpty() ? 0 : 2);
|
|
||||||
|
|
||||||
auto *line1 = new QLabel(firstLineText, container);
|
|
||||||
line1->setObjectName("stream-menu-line1");
|
|
||||||
layout->addWidget(line1);
|
|
||||||
|
|
||||||
if (!secondLine.isEmpty()) {
|
|
||||||
auto *line2 = new QLabel(secondLine, container);
|
|
||||||
line2->setObjectName("stream-menu-line2");
|
|
||||||
layout->addWidget(line2);
|
|
||||||
}
|
|
||||||
|
|
||||||
hostLayout->addWidget(container);
|
|
||||||
return host;
|
|
||||||
}
|
|
||||||
|
|
||||||
QWidget *streamMenuVisualWidget(QWidgetAction *action) {
|
|
||||||
if (!action || !action->defaultWidget()) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return action->defaultWidget()->findChild<QWidget *>(
|
|
||||||
"stream-menu-item", Qt::FindChildrenRecursively);
|
|
||||||
}
|
|
||||||
|
|
||||||
void refreshStreamMenuItemState(QAction *action, bool selected) {
|
|
||||||
action->setChecked(selected);
|
|
||||||
|
|
||||||
auto *widgetAction = qobject_cast<QWidgetAction *>(action);
|
|
||||||
if (!widgetAction) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto *visualWidget = streamMenuVisualWidget(widgetAction);
|
|
||||||
if (!visualWidget) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
visualWidget->setProperty("checked", selected);
|
|
||||||
visualWidget->style()->unpolish(visualWidget);
|
|
||||||
visualWidget->style()->polish(visualWidget);
|
|
||||||
visualWidget->update();
|
|
||||||
|
|
||||||
const auto labels = visualWidget->findChildren<QLabel *>();
|
|
||||||
for (auto *label : labels) {
|
|
||||||
label->style()->unpolish(label);
|
|
||||||
label->style()->polish(label);
|
|
||||||
label->update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ModernMenuButton::ModernMenuButton(QWidget *parent)
|
|
||||||
: QPushButton(parent), m_currentIndex(-1)
|
|
||||||
{
|
|
||||||
m_menu = new QMenu(this);
|
|
||||||
|
|
||||||
setObjectName("immersive-stream-btn");
|
|
||||||
m_menu->setObjectName("immersive-stream-menu");
|
|
||||||
m_menu->setProperty("customItemHighlight", true);
|
|
||||||
setStyleSheet("QPushButton::menu-indicator { image: none; }");
|
|
||||||
|
|
||||||
|
|
||||||
m_menu->setWindowFlags(Qt::Popup | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint);
|
|
||||||
m_menu->setAttribute(Qt::WA_TranslucentBackground, true);
|
|
||||||
m_menu->setAutoFillBackground(false);
|
|
||||||
PopupFade::attach(m_menu);
|
|
||||||
|
|
||||||
setCursor(Qt::PointingHandCursor);
|
|
||||||
|
|
||||||
connect(m_menu, &QMenu::triggered, this, &ModernMenuButton::onActionTriggered);
|
|
||||||
|
|
||||||
connect(this, &QPushButton::clicked, this, [this]() {
|
|
||||||
if (m_actions.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_menu->adjustSize();
|
|
||||||
QPoint pos = mapToGlobal(QPoint(width() / 2, height() + 4));
|
|
||||||
pos.setX(pos.x() - m_menu->sizeHint().width() / 2);
|
|
||||||
m_menu->exec(pos);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModernMenuButton::clear()
|
|
||||||
{
|
|
||||||
m_menu->clear();
|
|
||||||
m_actions.clear();
|
|
||||||
m_currentIndex = -1;
|
|
||||||
setText("");
|
|
||||||
|
|
||||||
|
|
||||||
setToolTip("");
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModernMenuButton::addItem(const QString& title, const QString& subtitle,
|
|
||||||
const QString& secondLine, const QVariant& userData)
|
|
||||||
{
|
|
||||||
|
|
||||||
QString firstLineText = title;
|
|
||||||
if (!subtitle.isEmpty()) {
|
|
||||||
firstLineText += " | " + subtitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto *widgetAction = new QWidgetAction(m_menu);
|
|
||||||
widgetAction->setDefaultWidget(
|
|
||||||
createStreamMenuItemWidget(firstLineText, secondLine));
|
|
||||||
widgetAction->setProperty("shortTitle", title);
|
|
||||||
widgetAction->setData(m_actions.size());
|
|
||||||
widgetAction->setProperty("realData", userData);
|
|
||||||
widgetAction->setProperty(
|
|
||||||
"fullText", secondLine.isEmpty() ? firstLineText
|
|
||||||
: firstLineText + "\n" + secondLine);
|
|
||||||
widgetAction->setCheckable(true);
|
|
||||||
|
|
||||||
m_menu->addAction(widgetAction);
|
|
||||||
m_actions.append(widgetAction);
|
|
||||||
|
|
||||||
if (m_actions.size() == 1) {
|
|
||||||
setCurrentIndex(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModernMenuButton::setCurrentIndex(int index)
|
|
||||||
{
|
|
||||||
if (index >= 0 && index < m_actions.size()) {
|
|
||||||
m_currentIndex = index;
|
|
||||||
updateAppearance();
|
|
||||||
} else if (index == -1) {
|
|
||||||
m_currentIndex = -1;
|
|
||||||
updateAppearance();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant ModernMenuButton::currentData() const
|
|
||||||
{
|
|
||||||
if (m_currentIndex >= 0 && m_currentIndex < m_actions.size()) {
|
|
||||||
return m_actions.at(m_currentIndex)->property("realData");
|
|
||||||
}
|
|
||||||
return QVariant();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModernMenuButton::onActionTriggered(QAction *action)
|
|
||||||
{
|
|
||||||
int index = action->data().toInt();
|
|
||||||
if (index != m_currentIndex) {
|
|
||||||
m_currentIndex = index;
|
|
||||||
updateAppearance();
|
|
||||||
Q_EMIT currentIndexChanged(m_currentIndex);
|
|
||||||
} else {
|
|
||||||
action->setChecked(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModernMenuButton::updateAppearance()
|
|
||||||
{
|
|
||||||
for (int i = 0; i < m_actions.size(); ++i) {
|
|
||||||
refreshStreamMenuItemState(m_actions.at(i), i == m_currentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_currentIndex >= 0 && m_currentIndex < m_actions.size()) {
|
|
||||||
QAction* currentAct = m_actions.at(m_currentIndex);
|
|
||||||
QString shortText = currentAct->property("shortTitle").toString();
|
|
||||||
|
|
||||||
setText(shortText + " ⯆");
|
|
||||||
|
|
||||||
|
|
||||||
setToolTip(currentAct->property("fullText").toString());
|
|
||||||
} else {
|
|
||||||
setText("");
|
|
||||||
setToolTip("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
#ifndef MODERNMENUBUTTON_H
|
|
||||||
#define MODERNMENUBUTTON_H
|
|
||||||
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QMenu>
|
|
||||||
#include <QVariant>
|
|
||||||
#include <QList>
|
|
||||||
|
|
||||||
class ModernMenuButton : public QPushButton {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit ModernMenuButton(QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
void clear();
|
|
||||||
void addItem(const QString& title, const QString& subtitle,
|
|
||||||
const QString& secondLine = QString(),
|
|
||||||
const QVariant& userData = QVariant());
|
|
||||||
void setCurrentIndex(int index);
|
|
||||||
|
|
||||||
int currentIndex() const { return m_currentIndex; }
|
|
||||||
QVariant currentData() const;
|
|
||||||
int count() const { return m_actions.size(); }
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void currentIndexChanged(int index);
|
|
||||||
|
|
||||||
private Q_SLOTS:
|
|
||||||
void onActionTriggered(QAction *action);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void updateAppearance();
|
|
||||||
|
|
||||||
QMenu* m_menu;
|
|
||||||
QList<QAction*> m_actions;
|
|
||||||
int m_currentIndex;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
#include "modernmenuscrollbar.h"
|
|
||||||
|
|
||||||
#include <QPainter>
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
constexpr int kHandleNormalWidth = 4;
|
|
||||||
constexpr int kHandleHoverWidth = 8;
|
|
||||||
constexpr int kHandleMinHeight = 28;
|
|
||||||
constexpr int kTrackVerticalPadding = 3;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
constexpr int kHandleAlphaNormal = 115;
|
|
||||||
constexpr int kHandleAlphaActive = 178;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ModernMenuScrollBar::ModernMenuScrollBar(Qt::Orientation orientation, QWidget *parent)
|
|
||||||
: QScrollBar(orientation, parent) {
|
|
||||||
|
|
||||||
|
|
||||||
setAttribute(Qt::WA_OpaquePaintEvent, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
ModernMenuScrollBar::ModernMenuScrollBar(QWidget *parent)
|
|
||||||
: QScrollBar(parent) {
|
|
||||||
setAttribute(Qt::WA_OpaquePaintEvent, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModernMenuScrollBar::enterEvent(QEnterEvent *event) {
|
|
||||||
m_hovered = true;
|
|
||||||
update();
|
|
||||||
QScrollBar::enterEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModernMenuScrollBar::leaveEvent(QEvent *event) {
|
|
||||||
m_hovered = false;
|
|
||||||
update();
|
|
||||||
QScrollBar::leaveEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
QRect ModernMenuScrollBar::computeHandleRect() const {
|
|
||||||
if (orientation() != Qt::Vertical) {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const int range = maximum() - minimum();
|
|
||||||
const int trackTop = kTrackVerticalPadding;
|
|
||||||
const int trackHeight = height() - 2 * kTrackVerticalPadding;
|
|
||||||
|
|
||||||
if (range <= 0 || trackHeight <= 0) {
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const int page = pageStep();
|
|
||||||
int handleHeight = (page > 0)
|
|
||||||
? static_cast<int>(static_cast<qint64>(trackHeight) * page / (range + page))
|
|
||||||
: kHandleMinHeight;
|
|
||||||
handleHeight = std::clamp(handleHeight, kHandleMinHeight, trackHeight);
|
|
||||||
|
|
||||||
const int availableTrack = trackHeight - handleHeight;
|
|
||||||
int handleY = trackTop;
|
|
||||||
if (availableTrack > 0) {
|
|
||||||
handleY = trackTop + static_cast<int>(
|
|
||||||
static_cast<qint64>(sliderPosition() - minimum()) * availableTrack / range);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const bool active = m_hovered || isSliderDown();
|
|
||||||
const int handleW = active ? kHandleHoverWidth : kHandleNormalWidth;
|
|
||||||
const int handleX = (width() - handleW) / 2;
|
|
||||||
|
|
||||||
return {handleX, handleY, handleW, handleHeight};
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModernMenuScrollBar::paintEvent(QPaintEvent *event) {
|
|
||||||
Q_UNUSED(event);
|
|
||||||
|
|
||||||
const QRect r = computeHandleRect();
|
|
||||||
if (r.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPainter painter(this);
|
|
||||||
painter.setRenderHint(QPainter::Antialiasing);
|
|
||||||
painter.setPen(Qt::NoPen);
|
|
||||||
|
|
||||||
const bool active = isSliderDown() || m_hovered;
|
|
||||||
const QColor handleColor(255, 255, 255, active ? kHandleAlphaActive : kHandleAlphaNormal);
|
|
||||||
|
|
||||||
painter.setBrush(handleColor);
|
|
||||||
|
|
||||||
const qreal radius = r.width() / 2.0;
|
|
||||||
painter.drawRoundedRect(QRectF(r), radius, radius);
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
#ifndef MODERNMENUSCROLLBAR_H
|
|
||||||
#define MODERNMENUSCROLLBAR_H
|
|
||||||
|
|
||||||
#include <QScrollBar>
|
|
||||||
#include <QPaintEvent>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ModernMenuScrollBar : public QScrollBar {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit ModernMenuScrollBar(Qt::Orientation orientation, QWidget *parent = nullptr);
|
|
||||||
explicit ModernMenuScrollBar(QWidget *parent = nullptr);
|
|
||||||
~ModernMenuScrollBar() override = default;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void paintEvent(QPaintEvent *event) override;
|
|
||||||
void enterEvent(QEnterEvent *event) override;
|
|
||||||
void leaveEvent(QEvent *event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
|
|
||||||
QRect computeHandleRect() const;
|
|
||||||
|
|
||||||
bool m_hovered = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
#include "modernnumberinput.h"
|
|
||||||
|
|
||||||
#include <QAbstractSpinBox>
|
|
||||||
|
|
||||||
ModernNumberInput::ModernNumberInput(QWidget *parent)
|
|
||||||
: QSpinBox(parent)
|
|
||||||
{
|
|
||||||
setObjectName("ModernNumberInput");
|
|
||||||
setRange(0, 10000);
|
|
||||||
setAlignment(Qt::AlignCenter);
|
|
||||||
setButtonSymbols(QAbstractSpinBox::NoButtons);
|
|
||||||
setKeyboardTracking(false);
|
|
||||||
setFixedSize(118, 34);
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
#ifndef MODERNNUMBERINPUT_H
|
|
||||||
#define MODERNNUMBERINPUT_H
|
|
||||||
|
|
||||||
#include <QSpinBox>
|
|
||||||
|
|
||||||
class ModernNumberInput : public QSpinBox
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit ModernNumberInput(QWidget *parent = nullptr);
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
#include "modernscrollpanel.h"
|
|
||||||
#include "modernmenuscrollbar.h"
|
|
||||||
#include <QGraphicsDropShadowEffect>
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QEvent>
|
|
||||||
#include <QFont>
|
|
||||||
#include <QFontMetrics>
|
|
||||||
#include <QScrollBar>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
constexpr int kMenuItemMinHeight = 42;
|
|
||||||
constexpr int kMenuItemVerticalPadding = 18;
|
|
||||||
constexpr int kMenuItemSpacing = 2;
|
|
||||||
constexpr int kScrollAreaFrameReserve = 2;
|
|
||||||
|
|
||||||
int menuItemHeight(const QFont &font)
|
|
||||||
{
|
|
||||||
const QFontMetrics fm(font);
|
|
||||||
return qMax(kMenuItemMinHeight, fm.height() + kMenuItemVerticalPadding);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ModernScrollPanel::ModernScrollPanel(QWidget *parent) : QFrame(parent), m_maxContentWidth(0) {
|
|
||||||
setObjectName("modernScrollMenuOuter");
|
|
||||||
|
|
||||||
setWindowFlags(Qt::Widget | Qt::FramelessWindowHint);
|
|
||||||
setAttribute(Qt::WA_TranslucentBackground);
|
|
||||||
setAttribute(Qt::WA_StyledBackground, true);
|
|
||||||
|
|
||||||
auto *shadow = new QGraphicsDropShadowEffect(this);
|
|
||||||
shadow->setBlurRadius(20);
|
|
||||||
shadow->setColor(QColor(0, 0, 0, 150));
|
|
||||||
shadow->setOffset(0, 4);
|
|
||||||
setGraphicsEffect(shadow);
|
|
||||||
|
|
||||||
m_mainLayout = new QVBoxLayout(this);
|
|
||||||
m_mainLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
m_mainLayout->setSpacing(0);
|
|
||||||
|
|
||||||
m_scrollArea = new QScrollArea(this);
|
|
||||||
m_scrollArea->setObjectName("modernMenuScrollArea");
|
|
||||||
m_scrollArea->setWidgetResizable(true);
|
|
||||||
m_scrollArea->setFrameShape(QFrame::NoFrame);
|
|
||||||
|
|
||||||
|
|
||||||
m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (auto *vp = m_scrollArea->viewport()) {
|
|
||||||
vp->setObjectName("modernMenuViewport");
|
|
||||||
vp->setAutoFillBackground(false);
|
|
||||||
vp->setAttribute(Qt::WA_StyledBackground, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
auto *vBar = new ModernMenuScrollBar(Qt::Vertical, m_scrollArea);
|
|
||||||
vBar->setObjectName("modernMenuScrollBar");
|
|
||||||
|
|
||||||
m_scrollArea->setVerticalScrollBar(vBar);
|
|
||||||
|
|
||||||
m_container = new QWidget(m_scrollArea);
|
|
||||||
m_container->setObjectName("modernMenuContainer");
|
|
||||||
m_container->setAttribute(Qt::WA_StyledBackground, true);
|
|
||||||
|
|
||||||
m_layout = new QVBoxLayout(m_container);
|
|
||||||
m_layout->setContentsMargins(4, 4, 4, 4);
|
|
||||||
|
|
||||||
|
|
||||||
m_layout->setSpacing(kMenuItemSpacing);
|
|
||||||
|
|
||||||
m_scrollArea->setWidget(m_container);
|
|
||||||
m_mainLayout->addWidget(m_scrollArea);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModernScrollPanel::addItem(const QString &text, const QVariant &userData, bool isSelected) {
|
|
||||||
auto *btn = new QPushButton(m_container);
|
|
||||||
btn->setObjectName("modernMenuItemBtn");
|
|
||||||
|
|
||||||
QPixmap pixmap(18, 18);
|
|
||||||
pixmap.fill(Qt::transparent);
|
|
||||||
|
|
||||||
if (isSelected) {
|
|
||||||
QPainter painter(&pixmap);
|
|
||||||
painter.setRenderHint(QPainter::Antialiasing);
|
|
||||||
painter.setPen(QColor(255, 255, 255, 255));
|
|
||||||
QFont font = painter.font();
|
|
||||||
font.setPixelSize(14);
|
|
||||||
font.setBold(true);
|
|
||||||
painter.setFont(font);
|
|
||||||
painter.drawText(pixmap.rect(), Qt::AlignCenter, "✓");
|
|
||||||
}
|
|
||||||
|
|
||||||
btn->setIcon(QIcon(pixmap));
|
|
||||||
btn->setText(text);
|
|
||||||
|
|
||||||
|
|
||||||
btn->setToolTip(text);
|
|
||||||
|
|
||||||
btn->setCursor(Qt::PointingHandCursor);
|
|
||||||
btn->setFocusPolicy(Qt::NoFocus);
|
|
||||||
btn->setFixedHeight(menuItemHeight(btn->font()));
|
|
||||||
|
|
||||||
|
|
||||||
QFontMetrics fm(btn->font());
|
|
||||||
m_maxContentWidth = qMax(m_maxContentWidth, fm.horizontalAdvance(text));
|
|
||||||
|
|
||||||
|
|
||||||
m_items.append({btn, text});
|
|
||||||
|
|
||||||
connect(btn, &QPushButton::clicked, this, [this, userData, text]() {
|
|
||||||
emit itemTriggered(userData, text);
|
|
||||||
});
|
|
||||||
|
|
||||||
m_layout->addWidget(btn);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModernScrollPanel::finalizeLayout(int maxHeight, int maxWidth) {
|
|
||||||
m_layout->addStretch();
|
|
||||||
|
|
||||||
m_container->adjustSize();
|
|
||||||
int contentHeight = m_container->sizeHint().height();
|
|
||||||
|
|
||||||
if (contentHeight < 10) {
|
|
||||||
contentHeight = 40;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_container->setMinimumHeight(contentHeight);
|
|
||||||
|
|
||||||
const int panelContentHeight = contentHeight + kScrollAreaFrameReserve;
|
|
||||||
int finalHeight = qMin(panelContentHeight, maxHeight);
|
|
||||||
bool needsVScroll = (panelContentHeight > maxHeight);
|
|
||||||
|
|
||||||
if (needsVScroll) {
|
|
||||||
m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
|
||||||
} else {
|
|
||||||
m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
int scrollBarBuffer = needsVScroll ? 12 : 0;
|
|
||||||
int requiredWidth = m_maxContentWidth + 45 + scrollBarBuffer;
|
|
||||||
|
|
||||||
|
|
||||||
int minWidth = 130;
|
|
||||||
|
|
||||||
|
|
||||||
int finalWidth = qBound(minWidth, requiredWidth, maxWidth);
|
|
||||||
|
|
||||||
|
|
||||||
int textAvailableWidth = finalWidth - 45 - scrollBarBuffer;
|
|
||||||
if (textAvailableWidth < 20) {
|
|
||||||
textAvailableWidth = 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
for (const auto& item : m_items) {
|
|
||||||
QFontMetrics fm(item.btn->font());
|
|
||||||
|
|
||||||
QString elidedText = fm.elidedText(item.fullText, Qt::ElideRight, textAvailableWidth);
|
|
||||||
item.btn->setText(elidedText);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
m_scrollArea->setFixedSize(finalWidth, finalHeight);
|
|
||||||
this->setFixedSize(finalWidth, finalHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModernScrollPanel::wheelEvent(QWheelEvent *event) {
|
|
||||||
QFrame::wheelEvent(event);
|
|
||||||
event->accept();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void ModernScrollPanel::paintEvent(QPaintEvent *event) {
|
|
||||||
Q_UNUSED(event);
|
|
||||||
|
|
||||||
QPainter painter(this);
|
|
||||||
painter.setRenderHint(QPainter::Antialiasing);
|
|
||||||
|
|
||||||
|
|
||||||
painter.setBrush(QColor(30, 30, 30, 242));
|
|
||||||
|
|
||||||
|
|
||||||
QPen pen(QColor(255, 255, 255, 26));
|
|
||||||
pen.setWidth(1);
|
|
||||||
painter.setPen(pen);
|
|
||||||
|
|
||||||
|
|
||||||
QRectF rf(rect().x() + 0.5, rect().y() + 0.5,
|
|
||||||
rect().width() - 1.0, rect().height() - 1.0);
|
|
||||||
painter.drawRoundedRect(rf, 6.0, 6.0);
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#ifndef MODERNSCROLLPANEL_H
|
|
||||||
#define MODERNSCROLLPANEL_H
|
|
||||||
|
|
||||||
#include <QFrame>
|
|
||||||
#include <QScrollArea>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QVariant>
|
|
||||||
#include <QString>
|
|
||||||
#include <QList>
|
|
||||||
#include <QWheelEvent>
|
|
||||||
#include <QPaintEvent>
|
|
||||||
|
|
||||||
class ModernScrollPanel : public QFrame {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit ModernScrollPanel(QWidget *parent = nullptr);
|
|
||||||
~ModernScrollPanel() override = default;
|
|
||||||
|
|
||||||
|
|
||||||
void addItem(const QString &text, const QVariant &userData, bool isSelected = false);
|
|
||||||
|
|
||||||
|
|
||||||
void finalizeLayout(int maxHeight, int maxWidth = 250);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void itemTriggered(const QVariant &userData, const QString &text);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void paintEvent(QPaintEvent *event) override;
|
|
||||||
void wheelEvent(QWheelEvent *event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
struct MenuItem {
|
|
||||||
QPushButton* btn;
|
|
||||||
QString fullText;
|
|
||||||
};
|
|
||||||
|
|
||||||
QVBoxLayout *m_mainLayout;
|
|
||||||
QScrollArea *m_scrollArea;
|
|
||||||
QWidget *m_container;
|
|
||||||
QVBoxLayout *m_layout;
|
|
||||||
|
|
||||||
QList<MenuItem> m_items;
|
|
||||||
int m_maxContentWidth;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
#include "modernsortbutton.h"
|
|
||||||
#include "../utils/popupfade.h"
|
|
||||||
#include <QAction>
|
|
||||||
|
|
||||||
ModernSortButton::ModernSortButton(QWidget *parent)
|
|
||||||
: QPushButton(parent), m_currentIndex(-1), m_descending(true)
|
|
||||||
{
|
|
||||||
m_menu = new QMenu(this);
|
|
||||||
PopupFade::attach(m_menu);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setObjectName("modern-sort-btn");
|
|
||||||
setCursor(Qt::PointingHandCursor);
|
|
||||||
|
|
||||||
connect(m_menu, &QMenu::triggered, this, &ModernSortButton::onActionTriggered);
|
|
||||||
|
|
||||||
|
|
||||||
connect(this, &QPushButton::clicked, this, [this]() {
|
|
||||||
m_menu->adjustSize();
|
|
||||||
QPoint pos = mapToGlobal(QPoint(width() / 2, height() + 6));
|
|
||||||
pos.setX(pos.x() - m_menu->sizeHint().width() / 2);
|
|
||||||
m_menu->exec(pos);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModernSortButton::setSortItems(const QStringList& items)
|
|
||||||
{
|
|
||||||
m_options.clear();
|
|
||||||
m_menu->clear();
|
|
||||||
|
|
||||||
for (const QString& item : items) {
|
|
||||||
addSortOption(item, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModernSortButton::addSortOption(const QString& name, const QString& key)
|
|
||||||
{
|
|
||||||
m_options.append({name, key});
|
|
||||||
QAction* action = m_menu->addAction(name);
|
|
||||||
|
|
||||||
action->setData(static_cast<int>(m_options.size() - 1));
|
|
||||||
action->setCheckable(true);
|
|
||||||
|
|
||||||
|
|
||||||
if (m_options.size() == 1) {
|
|
||||||
m_currentIndex = 0;
|
|
||||||
m_descending = true;
|
|
||||||
updateAppearance();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ModernSortButton::currentSortKey() const
|
|
||||||
{
|
|
||||||
if (m_currentIndex >= 0 && m_currentIndex < m_options.size()) {
|
|
||||||
return m_options.at(m_currentIndex).key;
|
|
||||||
}
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ModernSortButton::isAscending() const
|
|
||||||
{
|
|
||||||
return !m_descending;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void ModernSortButton::setCurrentIndex(int index)
|
|
||||||
{
|
|
||||||
if (index >= 0 && index < m_options.size() && m_currentIndex != index) {
|
|
||||||
m_currentIndex = index;
|
|
||||||
updateAppearance();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void ModernSortButton::setDescending(bool descending)
|
|
||||||
{
|
|
||||||
if (m_descending != descending) {
|
|
||||||
m_descending = descending;
|
|
||||||
updateAppearance();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModernSortButton::onActionTriggered(QAction *action)
|
|
||||||
{
|
|
||||||
int index = action->data().toInt();
|
|
||||||
if (index == m_currentIndex) {
|
|
||||||
|
|
||||||
m_descending = !m_descending;
|
|
||||||
} else {
|
|
||||||
|
|
||||||
m_currentIndex = index;
|
|
||||||
m_descending = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAppearance();
|
|
||||||
|
|
||||||
|
|
||||||
Q_EMIT sortChanged(currentSortKey(), isAscending());
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModernSortButton::updateAppearance()
|
|
||||||
{
|
|
||||||
const auto actionsList = m_menu->actions();
|
|
||||||
for (int i = 0; i < actionsList.size(); ++i) {
|
|
||||||
QAction* act = actionsList.at(i);
|
|
||||||
if (i == m_currentIndex) {
|
|
||||||
act->setChecked(true);
|
|
||||||
act->setText(m_options.at(i).name + (m_descending ? " ↓" : " ↑"));
|
|
||||||
} else {
|
|
||||||
act->setChecked(false);
|
|
||||||
act->setText(m_options.at(i).name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (m_currentIndex >= 0 && m_currentIndex < m_options.size()) {
|
|
||||||
setText(m_options.at(m_currentIndex).name + (m_descending ? " ↓" : " ↑"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
#ifndef MODERNSORTBUTTON_H
|
|
||||||
#define MODERNSORTBUTTON_H
|
|
||||||
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QMenu>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QList>
|
|
||||||
|
|
||||||
|
|
||||||
struct SortOption {
|
|
||||||
QString name;
|
|
||||||
QString key;
|
|
||||||
};
|
|
||||||
|
|
||||||
class ModernSortButton : public QPushButton {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit ModernSortButton(QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
|
|
||||||
void setSortItems(const QStringList& items);
|
|
||||||
|
|
||||||
|
|
||||||
void addSortOption(const QString& name, const QString& key);
|
|
||||||
|
|
||||||
|
|
||||||
QString currentSortKey() const;
|
|
||||||
bool isAscending() const;
|
|
||||||
|
|
||||||
int currentIndex() const { return m_currentIndex; }
|
|
||||||
bool isDescending() const { return m_descending; }
|
|
||||||
|
|
||||||
|
|
||||||
void setCurrentIndex(int index);
|
|
||||||
void setDescending(bool descending);
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
|
|
||||||
void sortChanged(const QString& sortKey, bool ascending);
|
|
||||||
|
|
||||||
private Q_SLOTS:
|
|
||||||
void onActionTriggered(QAction *action);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void updateAppearance();
|
|
||||||
|
|
||||||
QMenu* m_menu;
|
|
||||||
QList<SortOption> m_options;
|
|
||||||
int m_currentIndex;
|
|
||||||
bool m_descending;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,344 +0,0 @@
|
|||||||
#include "mpvwidget.h"
|
|
||||||
#include "mpvhttpstreamrelay.h"
|
|
||||||
#include "../utils/logredactionutils.h"
|
|
||||||
#include <QByteArray>
|
|
||||||
#include <QOpenGLContext>
|
|
||||||
#include <QMetaObject>
|
|
||||||
#include <QCoreApplication>
|
|
||||||
#include <QSurfaceFormat>
|
|
||||||
#include <QUrl>
|
|
||||||
#include <QVariantMap>
|
|
||||||
#include "api/proxymanager.h"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
QSurfaceFormat createMpvSurfaceFormat() {
|
|
||||||
QSurfaceFormat format = QSurfaceFormat::defaultFormat();
|
|
||||||
format.setRenderableType(QSurfaceFormat::OpenGL);
|
|
||||||
format.setVersion(3, 3);
|
|
||||||
format.setProfile(QSurfaceFormat::CoreProfile);
|
|
||||||
format.setDepthBufferSize(24);
|
|
||||||
format.setStencilBufferSize(8);
|
|
||||||
format.setSwapInterval(1);
|
|
||||||
format.setSwapBehavior(QSurfaceFormat::DoubleBuffer);
|
|
||||||
return format;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString glString(GLenum name) {
|
|
||||||
const auto *value = reinterpret_cast<const char *>(glGetString(name));
|
|
||||||
return value ? QString::fromLatin1(value) : QStringLiteral("<unknown>");
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
MpvWidget::MpvWidget(QWidget *parent)
|
|
||||||
: QOpenGLWidget(parent), m_mpv_gl(nullptr) {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this->setFormat(createMpvSurfaceFormat());
|
|
||||||
|
|
||||||
m_controller = new MpvController(this);
|
|
||||||
m_streamRelay = new MpvHttpStreamRelay(this);
|
|
||||||
connect(m_streamRelay, &MpvHttpStreamRelay::upstreamSpeedChanged, this,
|
|
||||||
&MpvWidget::networkSpeedChanged);
|
|
||||||
|
|
||||||
|
|
||||||
connect(m_controller, &MpvController::positionChanged, this, &MpvWidget::positionChanged);
|
|
||||||
connect(m_controller, &MpvController::durationChanged, this, &MpvWidget::durationChanged);
|
|
||||||
connect(m_controller, &MpvController::playbackStateChanged, this, &MpvWidget::playbackStateChanged);
|
|
||||||
connect(m_controller, &MpvController::errorOccurred, this, &MpvWidget::errorOccurred);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
m_controller->init();
|
|
||||||
}
|
|
||||||
|
|
||||||
MpvWidget::~MpvWidget() {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
shutdown();
|
|
||||||
if (m_controller) {
|
|
||||||
m_controller->deleteLater();
|
|
||||||
m_controller = nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void MpvWidget::shutdown() {
|
|
||||||
if (!m_controller) return;
|
|
||||||
|
|
||||||
|
|
||||||
this->blockSignals(true);
|
|
||||||
m_controller->blockSignals(true);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
m_controller->command(QVariantList{"stop"});
|
|
||||||
if (m_streamRelay) {
|
|
||||||
m_streamRelay->stop();
|
|
||||||
}
|
|
||||||
if (m_usingStreamRelay) {
|
|
||||||
m_usingStreamRelay = false;
|
|
||||||
Q_EMIT relayActiveChanged(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanupGL();
|
|
||||||
|
|
||||||
m_controller->forceCleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MpvWidget::cleanupGL() {
|
|
||||||
if (m_mpv_gl) {
|
|
||||||
|
|
||||||
|
|
||||||
if (isValid()) {
|
|
||||||
makeCurrent();
|
|
||||||
mpv_render_context_set_update_callback(m_mpv_gl, nullptr, nullptr);
|
|
||||||
mpv_render_context_free(m_mpv_gl);
|
|
||||||
doneCurrent();
|
|
||||||
} else {
|
|
||||||
qWarning() << "[MpvWidget] OpenGL context is not valid. Bypassing render context free.";
|
|
||||||
}
|
|
||||||
m_mpv_gl = nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void *MpvWidget::getProcAddress(void *ctx, const char *name) {
|
|
||||||
Q_UNUSED(ctx);
|
|
||||||
QOpenGLContext *glctx = QOpenGLContext::currentContext();
|
|
||||||
return glctx ? reinterpret_cast<void *>(glctx->getProcAddress(QByteArray(name))) : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MpvWidget::onMpvRenderUpdate(void *ctx) {
|
|
||||||
|
|
||||||
auto *self = static_cast<MpvWidget *>(ctx);
|
|
||||||
QMetaObject::invokeMethod(self, "maybeUpdate", Qt::QueuedConnection);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MpvWidget::maybeUpdate() {
|
|
||||||
Q_EMIT renderUpdateRequested();
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MpvWidget::initializeGL() {
|
|
||||||
initializeOpenGLFunctions();
|
|
||||||
|
|
||||||
QOpenGLContext *ctx = QOpenGLContext::currentContext();
|
|
||||||
if (!ctx) {
|
|
||||||
qCritical() << "[MpvWidget] Fatal Error: no current OpenGL context.";
|
|
||||||
emit errorOccurred(tr("当前环境缺乏必需的 OpenGL 硬件加速支持,视频渲染已禁用。"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QSurfaceFormat actualFormat = ctx->format();
|
|
||||||
qInfo().noquote()
|
|
||||||
<< QStringLiteral("[MpvWidget] OpenGL context")
|
|
||||||
<< "| requested: desktop OpenGL 3.3 core"
|
|
||||||
<< "| actual:"
|
|
||||||
<< QStringLiteral("%1.%2").arg(actualFormat.majorVersion()).arg(actualFormat.minorVersion())
|
|
||||||
<< "| profile:" << actualFormat.profile()
|
|
||||||
<< "| renderableType:" << actualFormat.renderableType()
|
|
||||||
<< "| isOpenGLES:" << ctx->isOpenGLES()
|
|
||||||
<< "| vendor:" << glString(GL_VENDOR)
|
|
||||||
<< "| renderer:" << glString(GL_RENDERER)
|
|
||||||
<< "| version:" << glString(GL_VERSION);
|
|
||||||
|
|
||||||
if (actualFormat.majorVersion() < 2) {
|
|
||||||
qCritical() << "Fatal Error: OpenGL version is too low.";
|
|
||||||
emit errorOccurred(tr("当前环境缺乏必需的 OpenGL 硬件加速支持,视频渲染已禁用。"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx->isOpenGLES() || actualFormat.majorVersion() < 3) {
|
|
||||||
qWarning() << "[MpvWidget] OpenGL context cannot safely upload hardware-decoded textures;"
|
|
||||||
<< "disabling MPV hardware decoding to avoid corrupted video"
|
|
||||||
<< "| isOpenGLES:" << ctx->isOpenGLES()
|
|
||||||
<< "| version:"
|
|
||||||
<< QStringLiteral("%1.%2").arg(actualFormat.majorVersion()).arg(actualFormat.minorVersion());
|
|
||||||
m_controller->setProperty(QStringLiteral("hwdec"), QStringLiteral("no"));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (m_mpv_gl) {
|
|
||||||
mpv_render_context_set_update_callback(m_mpv_gl, nullptr, nullptr);
|
|
||||||
mpv_render_context_free(m_mpv_gl);
|
|
||||||
m_mpv_gl = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
mpv_opengl_init_params gl_init_params{
|
|
||||||
getProcAddress,
|
|
||||||
nullptr
|
|
||||||
};
|
|
||||||
|
|
||||||
mpv_render_param params[]{
|
|
||||||
{MPV_RENDER_PARAM_API_TYPE, const_cast<char *>(MPV_RENDER_API_TYPE_OPENGL)},
|
|
||||||
{MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params},
|
|
||||||
{MPV_RENDER_PARAM_INVALID, nullptr}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (mpv_render_context_create(&m_mpv_gl, m_controller->mpv(), params) < 0) {
|
|
||||||
emit errorOccurred(tr("OpenGL rendering initialization failed."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mpv_render_context_set_update_callback(m_mpv_gl, onMpvRenderUpdate, this);
|
|
||||||
|
|
||||||
if (!m_pendingUrl.isEmpty()) {
|
|
||||||
loadMediaNow(m_pendingUrl, m_pendingServerId, true);
|
|
||||||
m_pendingUrl.clear();
|
|
||||||
m_pendingServerId.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MpvWidget::paintGL() {
|
|
||||||
if (!m_mpv_gl) return;
|
|
||||||
|
|
||||||
int w = static_cast<int>(width() * devicePixelRatio());
|
|
||||||
int h = static_cast<int>(height() * devicePixelRatio());
|
|
||||||
if (w == 0 || h == 0) return;
|
|
||||||
|
|
||||||
int fbo = defaultFramebufferObject();
|
|
||||||
int flip_y = 1;
|
|
||||||
|
|
||||||
mpv_opengl_fbo mpfbo{fbo, w, h, 0};
|
|
||||||
|
|
||||||
mpv_render_param params[] = {
|
|
||||||
{MPV_RENDER_PARAM_OPENGL_FBO, &mpfbo},
|
|
||||||
{MPV_RENDER_PARAM_FLIP_Y, &flip_y},
|
|
||||||
{MPV_RENDER_PARAM_INVALID, nullptr}
|
|
||||||
};
|
|
||||||
|
|
||||||
mpv_render_context_render(m_mpv_gl, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MpvWidget::resizeGL(int w, int h) {
|
|
||||||
Q_UNUSED(w);
|
|
||||||
Q_UNUSED(h);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void MpvWidget::loadMediaNow(const QString &url, const QString &serverId, bool wasPending) {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const QUrl loadQUrl(url);
|
|
||||||
const QNetworkProxy proxy =
|
|
||||||
serverId.isEmpty()
|
|
||||||
? ProxyManager::instance()->resolveForUrl(loadQUrl)
|
|
||||||
: ProxyManager::instance()->resolveForServerId(serverId);
|
|
||||||
const QString scheme = loadQUrl.scheme().toLower();
|
|
||||||
const bool isHttpStream =
|
|
||||||
scheme == QStringLiteral("http") || scheme == QStringLiteral("https");
|
|
||||||
const bool shouldRelay =
|
|
||||||
isHttpStream && proxy.type() != QNetworkProxy::NoProxy;
|
|
||||||
|
|
||||||
QString playbackUrl = url;
|
|
||||||
bool usingRelay = false;
|
|
||||||
if (shouldRelay && m_streamRelay) {
|
|
||||||
const QUrl localUrl = m_streamRelay->prepare(loadQUrl, serverId, proxy);
|
|
||||||
if (localUrl.isValid()) {
|
|
||||||
playbackUrl = localUrl.toString(QUrl::FullyEncoded);
|
|
||||||
usingRelay = true;
|
|
||||||
}
|
|
||||||
} else if (m_streamRelay) {
|
|
||||||
m_streamRelay->stop();
|
|
||||||
}
|
|
||||||
if (m_usingStreamRelay != usingRelay) {
|
|
||||||
m_usingStreamRelay = usingRelay;
|
|
||||||
Q_EMIT relayActiveChanged(usingRelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString mpvProxyValue =
|
|
||||||
usingRelay ? QString() : ProxyManager::toMpvHttpProxy(proxy);
|
|
||||||
const bool forceSeekable =
|
|
||||||
!usingRelay && !mpvProxyValue.isEmpty() && scheme == QStringLiteral("http");
|
|
||||||
|
|
||||||
m_controller->setProperty(QStringLiteral("http-proxy"), mpvProxyValue);
|
|
||||||
m_controller->setProperty(QStringLiteral("force-seekable"), forceSeekable);
|
|
||||||
|
|
||||||
QVariantMap loadOptions;
|
|
||||||
if (!mpvProxyValue.isEmpty()) {
|
|
||||||
loadOptions.insert(QStringLiteral("http-proxy"), mpvProxyValue);
|
|
||||||
}
|
|
||||||
if (forceSeekable) {
|
|
||||||
loadOptions.insert(QStringLiteral("force-seekable"), QStringLiteral("yes"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!usingRelay && !mpvProxyValue.isEmpty() && scheme == QStringLiteral("https")) {
|
|
||||||
qWarning() << "[MpvWidget] MPV http-proxy is not applied to HTTPS streams by libmpv"
|
|
||||||
<< "| url:" << LogRedactionUtils::url(loadQUrl)
|
|
||||||
<< "| serverId:" << (serverId.isEmpty()
|
|
||||||
? QStringLiteral("<none>")
|
|
||||||
: serverId);
|
|
||||||
}
|
|
||||||
|
|
||||||
qInfo() << (wasPending
|
|
||||||
? QStringLiteral("[MpvWidget] http-proxy applied (pending)")
|
|
||||||
: QStringLiteral("[MpvWidget] http-proxy applied"))
|
|
||||||
<< "| url:" << LogRedactionUtils::url(loadQUrl)
|
|
||||||
<< "| serverId:" << (serverId.isEmpty()
|
|
||||||
? QStringLiteral("<none>")
|
|
||||||
: serverId)
|
|
||||||
<< "| mpvProxyValue:" << LogRedactionUtils::proxy(mpvProxyValue)
|
|
||||||
<< "| relay:" << usingRelay
|
|
||||||
<< "| playbackUrl:" << (usingRelay
|
|
||||||
? playbackUrl
|
|
||||||
: QStringLiteral("<direct>"))
|
|
||||||
<< "| forceSeekable:" << forceSeekable;
|
|
||||||
|
|
||||||
QVariantList loadCommand;
|
|
||||||
if (loadOptions.isEmpty()) {
|
|
||||||
loadCommand = QVariantList{QStringLiteral("loadfile"), playbackUrl};
|
|
||||||
} else {
|
|
||||||
loadCommand = QVariantList{QStringLiteral("loadfile"), playbackUrl,
|
|
||||||
QStringLiteral("replace"), -1,
|
|
||||||
loadOptions};
|
|
||||||
}
|
|
||||||
|
|
||||||
const int err = m_controller->command(loadCommand, nullptr);
|
|
||||||
if (err < 0 && !loadOptions.isEmpty()) {
|
|
||||||
qWarning() << "[MpvWidget] loadfile with indexed per-file network options failed, retrying legacy form"
|
|
||||||
<< "| error:" << mpv_error_string(err);
|
|
||||||
const int legacyErr = m_controller->command(
|
|
||||||
QVariantList{QStringLiteral("loadfile"), playbackUrl,
|
|
||||||
QStringLiteral("replace"), loadOptions},
|
|
||||||
nullptr);
|
|
||||||
if (legacyErr < 0) {
|
|
||||||
qWarning() << "[MpvWidget] legacy loadfile per-file options failed, retrying plain loadfile"
|
|
||||||
<< "| error:" << mpv_error_string(legacyErr);
|
|
||||||
m_controller->command(QVariantList{QStringLiteral("loadfile"), playbackUrl}, nullptr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MpvWidget::loadMedia(const QString &url, const QString &serverId) {
|
|
||||||
|
|
||||||
if (!m_mpv_gl) {
|
|
||||||
m_pendingUrl = url;
|
|
||||||
m_pendingServerId = serverId;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loadMediaNow(url, serverId, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MpvWidget::play() {
|
|
||||||
m_controller->setProperty("pause", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MpvWidget::pause() {
|
|
||||||
m_controller->setProperty("pause", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MpvWidget::stop() {
|
|
||||||
m_controller->command(QVariantList{"stop"});
|
|
||||||
}
|
|
||||||
|
|
||||||
void MpvWidget::seek(double positionInSeconds) {
|
|
||||||
m_controller->command(QVariantList{"seek", positionInSeconds, "absolute"});
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
#ifndef MPVWIDGET_H
|
|
||||||
#define MPVWIDGET_H
|
|
||||||
|
|
||||||
#include <QOpenGLWidget>
|
|
||||||
#include <QOpenGLFunctions>
|
|
||||||
#include <QString>
|
|
||||||
#include <mpv/render_gl.h>
|
|
||||||
#include "mpvcontroller.h"
|
|
||||||
|
|
||||||
class MpvHttpStreamRelay;
|
|
||||||
|
|
||||||
class MpvWidget : public QOpenGLWidget, protected QOpenGLFunctions {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit MpvWidget(QWidget *parent = nullptr);
|
|
||||||
~MpvWidget() override;
|
|
||||||
|
|
||||||
|
|
||||||
void shutdown();
|
|
||||||
|
|
||||||
void loadMedia(const QString &url, const QString &serverId = QString());
|
|
||||||
void play();
|
|
||||||
void pause();
|
|
||||||
void stop();
|
|
||||||
void seek(double positionInSeconds);
|
|
||||||
|
|
||||||
MpvController* controller() const { return m_controller; }
|
|
||||||
|
|
||||||
signals:
|
|
||||||
|
|
||||||
void positionChanged(double position);
|
|
||||||
void durationChanged(double duration);
|
|
||||||
void playbackStateChanged(bool isPaused);
|
|
||||||
void networkSpeedChanged(qint64 bytesPerSecond);
|
|
||||||
void relayActiveChanged(bool active);
|
|
||||||
void renderUpdateRequested();
|
|
||||||
void errorOccurred(const QString &errorMsg);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void initializeGL() override;
|
|
||||||
void paintGL() override;
|
|
||||||
void resizeGL(int w, int h) override;
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void cleanupGL();
|
|
||||||
void maybeUpdate();
|
|
||||||
|
|
||||||
private:
|
|
||||||
static void onMpvRenderUpdate(void *ctx);
|
|
||||||
static void *getProcAddress(void *ctx, const char *name);
|
|
||||||
void loadMediaNow(const QString &url, const QString &serverId, bool wasPending);
|
|
||||||
|
|
||||||
MpvController *m_controller;
|
|
||||||
MpvHttpStreamRelay *m_streamRelay = nullptr;
|
|
||||||
bool m_usingStreamRelay = false;
|
|
||||||
mpv_render_context *m_mpv_gl;
|
|
||||||
|
|
||||||
|
|
||||||
QString m_pendingUrl;
|
|
||||||
QString m_pendingServerId;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
#include "playercombobox.h"
|
|
||||||
#include "../managers/thememanager.h"
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QStylePainter>
|
|
||||||
#include <QStyleOptionComboBox>
|
|
||||||
#include <QMouseEvent>
|
|
||||||
#include <QAbstractItemView>
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QApplication>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static constexpr int kItemHeight = 36;
|
|
||||||
static constexpr int kIconSize = 20;
|
|
||||||
static constexpr int kBadgeSize = 14;
|
|
||||||
static constexpr int kDeleteSize = 14;
|
|
||||||
static constexpr int kPadding = 10;
|
|
||||||
static constexpr int kSpacing = 6;
|
|
||||||
|
|
||||||
PlayerItemDelegate::PlayerItemDelegate(QObject *parent)
|
|
||||||
: QStyledItemDelegate(parent) {}
|
|
||||||
|
|
||||||
QSize PlayerItemDelegate::sizeHint(const QStyleOptionViewItem & ,
|
|
||||||
const QModelIndex & ) const {
|
|
||||||
return QSize(200, kItemHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
QRect PlayerItemDelegate::deleteButtonRect(const QStyleOptionViewItem &option) const {
|
|
||||||
|
|
||||||
int x = option.rect.right() - kPadding - kDeleteSize;
|
|
||||||
int y = option.rect.top() + (option.rect.height() - kDeleteSize) / 2;
|
|
||||||
return QRect(x, y, kDeleteSize, kDeleteSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
int PlayerItemDelegate::hitDeleteButton(const QStyleOptionViewItem &option,
|
|
||||||
const QPoint &pos) const {
|
|
||||||
QRect btnRect = deleteButtonRect(option);
|
|
||||||
|
|
||||||
btnRect.adjust(-4, -4, 4, 4);
|
|
||||||
return btnRect.contains(pos) ? 0 : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerItemDelegate::paint(QPainter *painter,
|
|
||||||
const QStyleOptionViewItem &option,
|
|
||||||
const QModelIndex &index) const {
|
|
||||||
painter->save();
|
|
||||||
painter->setRenderHint(QPainter::Antialiasing, true);
|
|
||||||
painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
|
|
||||||
|
|
||||||
bool isDark = ThemeManager::instance()->isDarkMode();
|
|
||||||
QString source = index.data(PlayerComboBox::SourceRole).toString();
|
|
||||||
QString path = index.data(Qt::UserRole).toString();
|
|
||||||
QString text = index.data(Qt::DisplayRole).toString();
|
|
||||||
bool isCustomEntry = (source == PlayerComboBox::SourceCustom);
|
|
||||||
|
|
||||||
|
|
||||||
if (option.state & QStyle::State_Selected) {
|
|
||||||
QColor sel = isDark ? QColor(59, 130, 246, 50) : QColor(59, 130, 246, 25);
|
|
||||||
painter->fillRect(option.rect, sel);
|
|
||||||
} else if (option.state & QStyle::State_MouseOver) {
|
|
||||||
QColor hov = isDark ? QColor(255, 255, 255, 18) : QColor(0, 0, 0, 12);
|
|
||||||
painter->fillRect(option.rect, hov);
|
|
||||||
}
|
|
||||||
|
|
||||||
int x = option.rect.left() + kPadding;
|
|
||||||
int centerY = option.rect.center().y();
|
|
||||||
|
|
||||||
|
|
||||||
if (!isCustomEntry && !path.isEmpty()) {
|
|
||||||
QFileInfo fi(path);
|
|
||||||
QIcon exeIcon = m_iconProvider.icon(fi);
|
|
||||||
if (!exeIcon.isNull()) {
|
|
||||||
QRect iconRect(x, centerY - kIconSize / 2, kIconSize, kIconSize);
|
|
||||||
exeIcon.paint(painter, iconRect);
|
|
||||||
}
|
|
||||||
x += kIconSize + kSpacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int rightX = option.rect.right() - kPadding;
|
|
||||||
|
|
||||||
|
|
||||||
if (!isCustomEntry) {
|
|
||||||
QRect delRect(rightX - kDeleteSize, centerY - kDeleteSize / 2,
|
|
||||||
kDeleteSize, kDeleteSize);
|
|
||||||
QIcon closeIcon = ThemeManager::getAdaptiveIcon(
|
|
||||||
":/svg/dark/close.svg",
|
|
||||||
(option.state & QStyle::State_MouseOver)
|
|
||||||
? (isDark ? QColor("#EF4444") : QColor("#DC2626"))
|
|
||||||
: (isDark ? QColor("#94A3B8") : QColor("#64748B")));
|
|
||||||
closeIcon.paint(painter, delRect);
|
|
||||||
rightX -= kDeleteSize + kSpacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (!isCustomEntry) {
|
|
||||||
QString badgeSvg = (source == PlayerComboBox::SourceAuto)
|
|
||||||
? ":/svg/dark/search.svg"
|
|
||||||
: ":/svg/dark/external-player.svg";
|
|
||||||
QRect badgeRect(rightX - kBadgeSize, centerY - kBadgeSize / 2,
|
|
||||||
kBadgeSize, kBadgeSize);
|
|
||||||
QIcon badgeIcon = ThemeManager::getAdaptiveIcon(
|
|
||||||
badgeSvg,
|
|
||||||
isDark ? QColor("#64748B") : QColor("#94A3B8"));
|
|
||||||
badgeIcon.paint(painter, badgeRect);
|
|
||||||
rightX -= kBadgeSize + kSpacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
QRect textRect(x, option.rect.top(), rightX - x - kSpacing, option.rect.height());
|
|
||||||
QColor textColor = isDark ? QColor("#E2E8F0") : QColor("#1E293B");
|
|
||||||
if (option.state & QStyle::State_Selected) {
|
|
||||||
textColor = isDark ? QColor("#93C5FD") : QColor("#2563EB");
|
|
||||||
}
|
|
||||||
painter->setPen(textColor);
|
|
||||||
QFont f = option.font;
|
|
||||||
f.setPointSizeF(9.5);
|
|
||||||
painter->setFont(f);
|
|
||||||
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine,
|
|
||||||
painter->fontMetrics().elidedText(text, Qt::ElideMiddle, textRect.width()));
|
|
||||||
|
|
||||||
painter->restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
PlayerComboBox::PlayerComboBox(QWidget *parent) : ModernComboBox(parent) {
|
|
||||||
m_delegate = new PlayerItemDelegate(this);
|
|
||||||
setItemDelegate(m_delegate);
|
|
||||||
|
|
||||||
|
|
||||||
view()->installEventFilter(this);
|
|
||||||
view()->viewport()->installEventFilter(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerComboBox::addPlayer(const QString &displayName, const QString &path,
|
|
||||||
const QString &source) {
|
|
||||||
int idx = count();
|
|
||||||
|
|
||||||
for (int i = 0; i < count(); ++i) {
|
|
||||||
if (itemData(i, SourceRole).toString() == SourceCustom) {
|
|
||||||
idx = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
insertItem(idx, displayName, path);
|
|
||||||
setItemData(idx, source, SourceRole);
|
|
||||||
updateGeometry();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerComboBox::removePlayer(int index) {
|
|
||||||
if (index < 0 || index >= count()) return;
|
|
||||||
|
|
||||||
QString source = itemData(index, SourceRole).toString();
|
|
||||||
if (source == SourceCustom) return;
|
|
||||||
|
|
||||||
QString path = itemData(index).toString();
|
|
||||||
bool wasCurrent = (currentIndex() == index);
|
|
||||||
|
|
||||||
blockSignals(true);
|
|
||||||
removeItem(index);
|
|
||||||
blockSignals(false);
|
|
||||||
updateGeometry();
|
|
||||||
|
|
||||||
if (wasCurrent && count() > 0) {
|
|
||||||
|
|
||||||
int newIdx = qMin(index, count() - 1);
|
|
||||||
if (itemData(newIdx, SourceRole).toString() == SourceCustom && newIdx > 0) {
|
|
||||||
newIdx--;
|
|
||||||
}
|
|
||||||
setCurrentIndex(newIdx);
|
|
||||||
}
|
|
||||||
|
|
||||||
emit playerRemoved(index, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerComboBox::removeAutoDetected() {
|
|
||||||
for (int i = count() - 1; i >= 0; --i) {
|
|
||||||
if (itemData(i, SourceRole).toString() == SourceAuto) {
|
|
||||||
removeItem(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateGeometry();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString PlayerComboBox::currentPlayerPath() const {
|
|
||||||
return currentData(Qt::UserRole).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerComboBox::paintEvent(QPaintEvent *) {
|
|
||||||
QStylePainter painter(this);
|
|
||||||
|
|
||||||
QStyleOptionComboBox opt;
|
|
||||||
initStyleOption(&opt);
|
|
||||||
|
|
||||||
|
|
||||||
QString text = opt.currentText;
|
|
||||||
opt.currentText = "";
|
|
||||||
|
|
||||||
|
|
||||||
painter.drawComplexControl(QStyle::CC_ComboBox, opt);
|
|
||||||
|
|
||||||
|
|
||||||
const int arrowWidth = 32;
|
|
||||||
const int iconSz = 18;
|
|
||||||
const int iconPad = 6;
|
|
||||||
QRect availRect = rect().adjusted(0, 0, -arrowWidth, 0);
|
|
||||||
|
|
||||||
QString path = currentData(Qt::UserRole).toString();
|
|
||||||
QString source = currentData(SourceRole).toString();
|
|
||||||
int textLeft = availRect.left();
|
|
||||||
|
|
||||||
if (source != SourceCustom && !path.isEmpty()) {
|
|
||||||
QFileInfo fi(path);
|
|
||||||
QIcon exeIcon = m_iconProvider.icon(fi);
|
|
||||||
if (!exeIcon.isNull()) {
|
|
||||||
int iconY = availRect.center().y() - iconSz / 2;
|
|
||||||
int iconX = availRect.left() + iconPad;
|
|
||||||
QRect iconRect(iconX, iconY, iconSz, iconSz);
|
|
||||||
exeIcon.paint(&painter, iconRect);
|
|
||||||
textLeft = iconX + iconSz + iconPad;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
QRect textRect(textLeft, availRect.top(), availRect.right() - textLeft, availRect.height());
|
|
||||||
painter.setPen(opt.palette.color(QPalette::ButtonText));
|
|
||||||
painter.setFont(this->font());
|
|
||||||
painter.drawText(textRect, Qt::AlignCenter | Qt::TextSingleLine, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
bool PlayerComboBox::eventFilter(QObject *obj, QEvent *event) {
|
|
||||||
if (event->type() == QEvent::MouseButtonRelease) {
|
|
||||||
auto *me = static_cast<QMouseEvent *>(event);
|
|
||||||
QAbstractItemView *v = view();
|
|
||||||
|
|
||||||
|
|
||||||
QModelIndex idx = v->indexAt(me->pos());
|
|
||||||
if (idx.isValid()) {
|
|
||||||
QString source = idx.data(SourceRole).toString();
|
|
||||||
if (source != SourceCustom) {
|
|
||||||
|
|
||||||
QStyleOptionViewItem opt;
|
|
||||||
opt.rect = v->visualRect(idx);
|
|
||||||
opt.state = QStyle::State_Enabled;
|
|
||||||
|
|
||||||
if (m_delegate->hitDeleteButton(opt, me->pos()) >= 0) {
|
|
||||||
|
|
||||||
removePlayer(idx.row());
|
|
||||||
event->accept();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ModernComboBox::eventFilter(obj, event);
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
#ifndef PLAYERCOMBOBOX_H
|
|
||||||
#define PLAYERCOMBOBOX_H
|
|
||||||
|
|
||||||
#include "moderncombobox.h"
|
|
||||||
#include <QStyledItemDelegate>
|
|
||||||
#include <QFileIconProvider>
|
|
||||||
|
|
||||||
|
|
||||||
class PlayerItemDelegate : public QStyledItemDelegate {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit PlayerItemDelegate(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
void paint(QPainter *painter, const QStyleOptionViewItem &option,
|
|
||||||
const QModelIndex &index) const override;
|
|
||||||
QSize sizeHint(const QStyleOptionViewItem &option,
|
|
||||||
const QModelIndex &index) const override;
|
|
||||||
|
|
||||||
|
|
||||||
int hitDeleteButton(const QStyleOptionViewItem &option,
|
|
||||||
const QPoint &pos) const;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void deleteClicked(int index);
|
|
||||||
|
|
||||||
private:
|
|
||||||
QRect deleteButtonRect(const QStyleOptionViewItem &option) const;
|
|
||||||
mutable QFileIconProvider m_iconProvider;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PlayerComboBox : public ModernComboBox {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
|
|
||||||
static constexpr const char *SourceAuto = "auto";
|
|
||||||
static constexpr const char *SourceManual = "manual";
|
|
||||||
static constexpr const char *SourceCustom = "custom";
|
|
||||||
|
|
||||||
static constexpr int SourceRole = Qt::UserRole + 1;
|
|
||||||
|
|
||||||
explicit PlayerComboBox(QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
|
|
||||||
void addPlayer(const QString &displayName, const QString &path,
|
|
||||||
const QString &source);
|
|
||||||
|
|
||||||
|
|
||||||
void removePlayer(int index);
|
|
||||||
|
|
||||||
|
|
||||||
void removeAutoDetected();
|
|
||||||
|
|
||||||
|
|
||||||
QString currentPlayerPath() const;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void playerRemoved(int index, const QString &path);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
|
||||||
void paintEvent(QPaintEvent *event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
PlayerItemDelegate *m_delegate;
|
|
||||||
QFileIconProvider m_iconProvider;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "playerdanmakucontroller.h"
|
#include "playerdanmakucontroller.h"
|
||||||
|
|
||||||
#include "mpvwidget.h"
|
#include "mpvcontroller.h"
|
||||||
#include "nativedanmakuoverlay.h"
|
#include "idanmakusurface.h"
|
||||||
#include "../utils/danmakurendererutils.h"
|
#include "../utils/danmakurendererutils.h"
|
||||||
#include "../utils/subtitlestyleutils.h"
|
#include "../utils/subtitlestyleutils.h"
|
||||||
#include <config/config_keys.h>
|
#include <config/config_keys.h>
|
||||||
@@ -81,32 +81,32 @@ bool shouldInterpolationBeEnabled()
|
|||||||
}
|
}
|
||||||
|
|
||||||
PlayerDanmakuController::PlayerDanmakuController(QEmbyCore *core,
|
PlayerDanmakuController::PlayerDanmakuController(QEmbyCore *core,
|
||||||
MpvWidget *mpvWidget,
|
MpvController *mpvController,
|
||||||
NativeDanmakuOverlay *nativeDanmakuOverlay,
|
IDanmakuSurface *danmakuSurface,
|
||||||
QObject *parent)
|
QObject *parent)
|
||||||
: QObject(parent),
|
: QObject(parent),
|
||||||
m_core(core),
|
m_core(core),
|
||||||
m_mpvWidget(mpvWidget),
|
m_mpvController(mpvController),
|
||||||
m_nativeDanmakuOverlay(nativeDanmakuOverlay)
|
m_danmakuSurface(danmakuSurface)
|
||||||
{
|
{
|
||||||
if (!m_mpvWidget || !m_mpvWidget->controller()) {
|
if (!m_mpvController) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_nativeDanmakuOverlay && m_core && m_core->danmakuService()) {
|
if (m_danmakuSurface && m_core && m_core->danmakuService()) {
|
||||||
m_nativeDanmakuOverlay->setRenderOptions(
|
m_danmakuSurface->setRenderOptions(
|
||||||
m_core->danmakuService()->renderOptions());
|
m_core->danmakuService()->renderOptions());
|
||||||
m_nativeDanmakuOverlay->setDanmakuVisible(false);
|
m_danmakuSurface->setDanmakuVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(m_mpvWidget->controller(), &MpvController::fileLoaded, this,
|
connect(m_mpvController, &MpvController::fileLoaded, this,
|
||||||
[this]() {
|
[this]() {
|
||||||
m_fileLoaded = true;
|
m_fileLoaded = true;
|
||||||
syncSubtitleSelectionFromTrackList();
|
syncSubtitleSelectionFromTrackList();
|
||||||
updateDanmakuPresentation();
|
updateDanmakuPresentation();
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(m_mpvWidget->controller(), &MpvController::propertyChanged, this,
|
connect(m_mpvController, &MpvController::propertyChanged, this,
|
||||||
[this](const QString &property, const QVariant &) {
|
[this](const QString &property, const QVariant &) {
|
||||||
if (property == QLatin1String("track-list")) {
|
if (property == QLatin1String("track-list")) {
|
||||||
onTrackListChanged();
|
onTrackListChanged();
|
||||||
@@ -132,6 +132,17 @@ PlayerDanmakuController::PlayerDanmakuController(QEmbyCore *core,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlayerDanmakuController::setDanmakuSurface(IDanmakuSurface *surface)
|
||||||
|
{
|
||||||
|
m_danmakuSurface = surface;
|
||||||
|
if (m_danmakuSurface && m_core && m_core->danmakuService()) {
|
||||||
|
m_danmakuSurface->setRenderOptions(
|
||||||
|
m_core->danmakuService()->renderOptions());
|
||||||
|
m_danmakuSurface->setDanmakuVisible(false);
|
||||||
|
updateDanmakuPresentation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void PlayerDanmakuController::setPlaybackContext(const PlayerLaunchContext &context)
|
void PlayerDanmakuController::setPlaybackContext(const PlayerLaunchContext &context)
|
||||||
{
|
{
|
||||||
++m_requestSerial;
|
++m_requestSerial;
|
||||||
@@ -152,8 +163,8 @@ void PlayerDanmakuController::setPlaybackContext(const PlayerLaunchContext &cont
|
|||||||
m_visible = true;
|
m_visible = true;
|
||||||
m_loading = false;
|
m_loading = false;
|
||||||
m_nativePayloadDirty = false;
|
m_nativePayloadDirty = false;
|
||||||
if (m_nativeDanmakuOverlay) {
|
if (m_danmakuSurface) {
|
||||||
m_nativeDanmakuOverlay->clearDanmaku();
|
m_danmakuSurface->clearDanmaku();
|
||||||
}
|
}
|
||||||
|
|
||||||
qDebug().noquote()
|
qDebug().noquote()
|
||||||
@@ -202,8 +213,8 @@ void PlayerDanmakuController::clearPlaybackContext()
|
|||||||
m_visible = true;
|
m_visible = true;
|
||||||
m_loading = false;
|
m_loading = false;
|
||||||
m_nativePayloadDirty = false;
|
m_nativePayloadDirty = false;
|
||||||
if (m_nativeDanmakuOverlay) {
|
if (m_danmakuSurface) {
|
||||||
m_nativeDanmakuOverlay->clearDanmaku();
|
m_danmakuSurface->clearDanmaku();
|
||||||
}
|
}
|
||||||
qDebug() << "[Danmaku][Player] Cleared playback context";
|
qDebug() << "[Danmaku][Player] Cleared playback context";
|
||||||
emit stateChanged();
|
emit stateChanged();
|
||||||
@@ -282,12 +293,12 @@ QString PlayerDanmakuController::activeEndpointId() const
|
|||||||
QList<QVariantMap> PlayerDanmakuController::contentSubtitleTracks() const
|
QList<QVariantMap> PlayerDanmakuController::contentSubtitleTracks() const
|
||||||
{
|
{
|
||||||
QList<QVariantMap> tracks;
|
QList<QVariantMap> tracks;
|
||||||
if (!m_mpvWidget || !m_mpvWidget->controller()) {
|
if (!m_mpvController) {
|
||||||
return tracks;
|
return tracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QVariantList trackList =
|
const QVariantList trackList =
|
||||||
m_mpvWidget->controller()->getProperty(QStringLiteral("track-list")).toList();
|
m_mpvController->getProperty(QStringLiteral("track-list")).toList();
|
||||||
for (const QVariant &value : trackList) {
|
for (const QVariant &value : trackList) {
|
||||||
QVariantMap trackMap = value.toMap();
|
QVariantMap trackMap = value.toMap();
|
||||||
if (trackMap.value(QStringLiteral("type")).toString() !=
|
if (trackMap.value(QStringLiteral("type")).toString() !=
|
||||||
@@ -308,7 +319,7 @@ QList<QVariantMap> PlayerDanmakuController::contentSubtitleTracks() const
|
|||||||
|
|
||||||
void PlayerDanmakuController::selectSubtitleTrack(const QVariant &data)
|
void PlayerDanmakuController::selectSubtitleTrack(const QVariant &data)
|
||||||
{
|
{
|
||||||
if (!m_mpvWidget || !m_mpvWidget->controller()) {
|
if (!m_mpvController) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,8 +408,8 @@ void PlayerDanmakuController::reload(const QString &manualKeyword)
|
|||||||
m_commentCount = 0;
|
m_commentCount = 0;
|
||||||
m_loading = false;
|
m_loading = false;
|
||||||
m_nativePayloadDirty = false;
|
m_nativePayloadDirty = false;
|
||||||
if (m_nativeDanmakuOverlay) {
|
if (m_danmakuSurface) {
|
||||||
m_nativeDanmakuOverlay->clearDanmaku();
|
m_danmakuSurface->clearDanmaku();
|
||||||
}
|
}
|
||||||
qDebug().noquote()
|
qDebug().noquote()
|
||||||
<< "[Danmaku][Player] Reload requested"
|
<< "[Danmaku][Player] Reload requested"
|
||||||
@@ -433,8 +444,8 @@ void PlayerDanmakuController::loadFromCandidate(
|
|||||||
m_commentCount = 0;
|
m_commentCount = 0;
|
||||||
m_loading = false;
|
m_loading = false;
|
||||||
m_nativePayloadDirty = false;
|
m_nativePayloadDirty = false;
|
||||||
if (m_nativeDanmakuOverlay) {
|
if (m_danmakuSurface) {
|
||||||
m_nativeDanmakuOverlay->clearDanmaku();
|
m_danmakuSurface->clearDanmaku();
|
||||||
}
|
}
|
||||||
qDebug().noquote()
|
qDebug().noquote()
|
||||||
<< "[Danmaku][Player] Load candidate requested"
|
<< "[Danmaku][Player] Load candidate requested"
|
||||||
@@ -549,8 +560,8 @@ void PlayerDanmakuController::attachDanmakuTrack()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_fileLoaded || m_assFilePath.isEmpty() || !m_mpvWidget ||
|
if (!m_fileLoaded || m_assFilePath.isEmpty() || !m_mpvController ||
|
||||||
!m_mpvWidget->controller()) {
|
!m_mpvController) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,7 +569,7 @@ void PlayerDanmakuController::attachDanmakuTrack()
|
|||||||
qDebug().noquote()
|
qDebug().noquote()
|
||||||
<< "[Danmaku][Player] Attach danmaku track"
|
<< "[Danmaku][Player] Attach danmaku track"
|
||||||
<< "| path:" << m_assFilePath;
|
<< "| path:" << m_assFilePath;
|
||||||
m_mpvWidget->controller()->command(QVariantList{
|
m_mpvController->command(QVariantList{
|
||||||
QStringLiteral("sub-add"),
|
QStringLiteral("sub-add"),
|
||||||
QDir::toNativeSeparators(m_assFilePath),
|
QDir::toNativeSeparators(m_assFilePath),
|
||||||
QStringLiteral("auto"),
|
QStringLiteral("auto"),
|
||||||
@@ -569,12 +580,12 @@ void PlayerDanmakuController::attachDanmakuTrack()
|
|||||||
|
|
||||||
void PlayerDanmakuController::refreshDanmakuTrackId(int remainingRetries)
|
void PlayerDanmakuController::refreshDanmakuTrackId(int remainingRetries)
|
||||||
{
|
{
|
||||||
if (!m_mpvWidget || !m_mpvWidget->controller()) {
|
if (!m_mpvController) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QVariantList trackList =
|
const QVariantList trackList =
|
||||||
m_mpvWidget->controller()->getProperty(QStringLiteral("track-list")).toList();
|
m_mpvController->getProperty(QStringLiteral("track-list")).toList();
|
||||||
for (const QVariant &value : trackList) {
|
for (const QVariant &value : trackList) {
|
||||||
const QVariantMap trackMap = value.toMap();
|
const QVariantMap trackMap = value.toMap();
|
||||||
if (trackMap.value(QStringLiteral("type")).toString() !=
|
if (trackMap.value(QStringLiteral("type")).toString() !=
|
||||||
@@ -607,7 +618,7 @@ void PlayerDanmakuController::refreshDanmakuTrackId(int remainingRetries)
|
|||||||
|
|
||||||
void PlayerDanmakuController::removeDanmakuTrack()
|
void PlayerDanmakuController::removeDanmakuTrack()
|
||||||
{
|
{
|
||||||
if (!m_mpvWidget || !m_mpvWidget->controller()) {
|
if (!m_mpvController) {
|
||||||
m_danmakuTrackId = -1;
|
m_danmakuTrackId = -1;
|
||||||
m_motionStabilityProfileApplied = false;
|
m_motionStabilityProfileApplied = false;
|
||||||
return;
|
return;
|
||||||
@@ -617,7 +628,7 @@ void PlayerDanmakuController::removeDanmakuTrack()
|
|||||||
qDebug().noquote()
|
qDebug().noquote()
|
||||||
<< "[Danmaku][Player] Remove danmaku track"
|
<< "[Danmaku][Player] Remove danmaku track"
|
||||||
<< "| trackId:" << m_danmakuTrackId;
|
<< "| trackId:" << m_danmakuTrackId;
|
||||||
m_mpvWidget->controller()->command(
|
m_mpvController->command(
|
||||||
QVariantList{QStringLiteral("sub-remove"), m_danmakuTrackId});
|
QVariantList{QStringLiteral("sub-remove"), m_danmakuTrackId});
|
||||||
}
|
}
|
||||||
m_danmakuTrackId = -1;
|
m_danmakuTrackId = -1;
|
||||||
@@ -626,7 +637,7 @@ void PlayerDanmakuController::removeDanmakuTrack()
|
|||||||
|
|
||||||
bool PlayerDanmakuController::prefersNativeRenderer() const
|
bool PlayerDanmakuController::prefersNativeRenderer() const
|
||||||
{
|
{
|
||||||
return m_nativeDanmakuOverlay &&
|
return m_danmakuSurface &&
|
||||||
DanmakuRendererUtils::isNativeRenderer(
|
DanmakuRendererUtils::isNativeRenderer(
|
||||||
ConfigStore::instance()->get<QString>(
|
ConfigStore::instance()->get<QString>(
|
||||||
ConfigKeys::PlayerDanmakuRenderer,
|
ConfigKeys::PlayerDanmakuRenderer,
|
||||||
@@ -641,21 +652,21 @@ bool PlayerDanmakuController::shouldUseNativeRenderer() const
|
|||||||
void PlayerDanmakuController::updateDanmakuPresentation()
|
void PlayerDanmakuController::updateDanmakuPresentation()
|
||||||
{
|
{
|
||||||
const bool nativeRendererPreferred = prefersNativeRenderer();
|
const bool nativeRendererPreferred = prefersNativeRenderer();
|
||||||
if (nativeRendererPreferred && m_nativeDanmakuOverlay && m_core &&
|
if (nativeRendererPreferred && m_danmakuSurface && m_core &&
|
||||||
m_core->danmakuService()) {
|
m_core->danmakuService()) {
|
||||||
m_nativeDanmakuOverlay->setRenderOptions(
|
m_danmakuSurface->setRenderOptions(
|
||||||
m_core->danmakuService()->renderOptions());
|
m_core->danmakuService()->renderOptions());
|
||||||
if (m_nativePayloadDirty) {
|
if (m_nativePayloadDirty) {
|
||||||
if (!m_commentPayload.isEmpty()) {
|
if (!m_commentPayload.isEmpty()) {
|
||||||
m_nativeDanmakuOverlay->setComments(m_commentPayload);
|
m_danmakuSurface->setComments(m_commentPayload);
|
||||||
} else {
|
} else {
|
||||||
m_nativeDanmakuOverlay->clearDanmaku();
|
m_danmakuSurface->clearDanmaku();
|
||||||
}
|
}
|
||||||
m_nativePayloadDirty = false;
|
m_nativePayloadDirty = false;
|
||||||
}
|
}
|
||||||
} else if (m_nativeDanmakuOverlay && m_nativePayloadDirty &&
|
} else if (m_danmakuSurface && m_nativePayloadDirty &&
|
||||||
m_commentPayload.isEmpty()) {
|
m_commentPayload.isEmpty()) {
|
||||||
m_nativeDanmakuOverlay->clearDanmaku();
|
m_danmakuSurface->clearDanmaku();
|
||||||
m_nativePayloadDirty = false;
|
m_nativePayloadDirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,8 +680,8 @@ void PlayerDanmakuController::updateDanmakuPresentation()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_nativeDanmakuOverlay) {
|
if (m_danmakuSurface) {
|
||||||
m_nativeDanmakuOverlay->setDanmakuVisible(false);
|
m_danmakuSurface->setDanmakuVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDanmakuVisible() && m_fileLoaded && !m_assFilePath.isEmpty() &&
|
if (isDanmakuVisible() && m_fileLoaded && !m_assFilePath.isEmpty() &&
|
||||||
@@ -684,11 +695,11 @@ void PlayerDanmakuController::updateDanmakuPresentation()
|
|||||||
|
|
||||||
void PlayerDanmakuController::applyDanmakuMotionStabilityProfile()
|
void PlayerDanmakuController::applyDanmakuMotionStabilityProfile()
|
||||||
{
|
{
|
||||||
if (!m_mpvWidget || !m_mpvWidget->controller()) {
|
if (!m_mpvController) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto *controller = m_mpvWidget->controller();
|
MpvController *controller = m_mpvController.data();
|
||||||
auto setOptionWithLog = [controller](const QString &name,
|
auto setOptionWithLog = [controller](const QString &name,
|
||||||
const QVariant &value) {
|
const QVariant &value) {
|
||||||
const int err = controller->setProperty(name, value);
|
const int err = controller->setProperty(name, value);
|
||||||
@@ -724,12 +735,12 @@ void PlayerDanmakuController::applyDanmakuMotionStabilityProfile()
|
|||||||
|
|
||||||
void PlayerDanmakuController::clearDanmakuMotionStabilityProfile()
|
void PlayerDanmakuController::clearDanmakuMotionStabilityProfile()
|
||||||
{
|
{
|
||||||
if (!m_mpvWidget || !m_mpvWidget->controller()) {
|
if (!m_mpvController) {
|
||||||
m_motionStabilityProfileApplied = false;
|
m_motionStabilityProfileApplied = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto *controller = m_mpvWidget->controller();
|
MpvController *controller = m_mpvController.data();
|
||||||
auto setOptionWithLog = [controller](const QString &name,
|
auto setOptionWithLog = [controller](const QString &name,
|
||||||
const QVariant &value) {
|
const QVariant &value) {
|
||||||
const int err = controller->setProperty(name, value);
|
const int err = controller->setProperty(name, value);
|
||||||
@@ -767,7 +778,7 @@ void PlayerDanmakuController::clearDanmakuMotionStabilityProfile()
|
|||||||
|
|
||||||
void PlayerDanmakuController::applyTrackSelection()
|
void PlayerDanmakuController::applyTrackSelection()
|
||||||
{
|
{
|
||||||
if (!m_mpvWidget || !m_mpvWidget->controller()) {
|
if (!m_mpvController) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,10 +787,10 @@ void PlayerDanmakuController::applyTrackSelection()
|
|||||||
const bool useNativeRenderer =
|
const bool useNativeRenderer =
|
||||||
shouldUseNativeRenderer() && isDanmakuVisible();
|
shouldUseNativeRenderer() && isDanmakuVisible();
|
||||||
|
|
||||||
if (m_nativeDanmakuOverlay) {
|
if (m_danmakuSurface) {
|
||||||
m_nativeDanmakuOverlay->setBottomSubtitleProtected(
|
m_danmakuSurface->setBottomSubtitleProtected(
|
||||||
useNativeRenderer && dualSubtitle && m_selectedSubtitleTrackId > 0);
|
useNativeRenderer && dualSubtitle && m_selectedSubtitleTrackId > 0);
|
||||||
m_nativeDanmakuOverlay->setDanmakuVisible(useNativeRenderer);
|
m_danmakuSurface->setDanmakuVisible(useNativeRenderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useNativeRenderer) {
|
if (useNativeRenderer) {
|
||||||
@@ -788,17 +799,17 @@ void PlayerDanmakuController::applyTrackSelection()
|
|||||||
<< "| contentSubtitleTrackId:" << m_selectedSubtitleTrackId
|
<< "| contentSubtitleTrackId:" << m_selectedSubtitleTrackId
|
||||||
<< "| dualSubtitle:" << dualSubtitle
|
<< "| dualSubtitle:" << dualSubtitle
|
||||||
<< "| commentCount:" << m_commentPayload.size();
|
<< "| commentCount:" << m_commentPayload.size();
|
||||||
m_mpvWidget->controller()->setProperty(QStringLiteral("secondary-sid"),
|
m_mpvController->setProperty(QStringLiteral("secondary-sid"),
|
||||||
QStringLiteral("no"));
|
QStringLiteral("no"));
|
||||||
if (dualSubtitle && m_selectedSubtitleTrackId > 0) {
|
if (dualSubtitle && m_selectedSubtitleTrackId > 0) {
|
||||||
m_mpvWidget->controller()->setProperty(QStringLiteral("sid"),
|
m_mpvController->setProperty(QStringLiteral("sid"),
|
||||||
m_selectedSubtitleTrackId);
|
m_selectedSubtitleTrackId);
|
||||||
} else {
|
} else {
|
||||||
m_mpvWidget->controller()->setProperty(QStringLiteral("sid"),
|
m_mpvController->setProperty(QStringLiteral("sid"),
|
||||||
QStringLiteral("no"));
|
QStringLiteral("no"));
|
||||||
}
|
}
|
||||||
clearDanmakuMotionStabilityProfile();
|
clearDanmakuMotionStabilityProfile();
|
||||||
SubtitleStyleUtils::applyToController(m_mpvWidget->controller(), false);
|
SubtitleStyleUtils::applyToController(m_mpvController, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -808,36 +819,36 @@ void PlayerDanmakuController::applyTrackSelection()
|
|||||||
<< "| danmakuTrackId:" << m_danmakuTrackId
|
<< "| danmakuTrackId:" << m_danmakuTrackId
|
||||||
<< "| contentSubtitleTrackId:" << m_selectedSubtitleTrackId
|
<< "| contentSubtitleTrackId:" << m_selectedSubtitleTrackId
|
||||||
<< "| dualSubtitle:" << dualSubtitle;
|
<< "| dualSubtitle:" << dualSubtitle;
|
||||||
m_mpvWidget->controller()->setProperty(QStringLiteral("sid"),
|
m_mpvController->setProperty(QStringLiteral("sid"),
|
||||||
m_danmakuTrackId);
|
m_danmakuTrackId);
|
||||||
if (dualSubtitle && m_selectedSubtitleTrackId > 0) {
|
if (dualSubtitle && m_selectedSubtitleTrackId > 0) {
|
||||||
m_mpvWidget->controller()->setProperty(QStringLiteral("secondary-sub-pos"),
|
m_mpvController->setProperty(QStringLiteral("secondary-sub-pos"),
|
||||||
92);
|
92);
|
||||||
m_mpvWidget->controller()->setProperty(QStringLiteral("secondary-sid"),
|
m_mpvController->setProperty(QStringLiteral("secondary-sid"),
|
||||||
m_selectedSubtitleTrackId);
|
m_selectedSubtitleTrackId);
|
||||||
} else {
|
} else {
|
||||||
m_mpvWidget->controller()->setProperty(QStringLiteral("secondary-sid"),
|
m_mpvController->setProperty(QStringLiteral("secondary-sid"),
|
||||||
QStringLiteral("no"));
|
QStringLiteral("no"));
|
||||||
}
|
}
|
||||||
applyDanmakuMotionStabilityProfile();
|
applyDanmakuMotionStabilityProfile();
|
||||||
SubtitleStyleUtils::applyToController(m_mpvWidget->controller(), true);
|
SubtitleStyleUtils::applyToController(m_mpvController, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
qDebug().noquote()
|
qDebug().noquote()
|
||||||
<< "[Danmaku][Player] Apply regular subtitle selection"
|
<< "[Danmaku][Player] Apply regular subtitle selection"
|
||||||
<< "| contentSubtitleTrackId:" << m_selectedSubtitleTrackId;
|
<< "| contentSubtitleTrackId:" << m_selectedSubtitleTrackId;
|
||||||
m_mpvWidget->controller()->setProperty(QStringLiteral("secondary-sid"),
|
m_mpvController->setProperty(QStringLiteral("secondary-sid"),
|
||||||
QStringLiteral("no"));
|
QStringLiteral("no"));
|
||||||
if (m_selectedSubtitleTrackId > 0) {
|
if (m_selectedSubtitleTrackId > 0) {
|
||||||
m_mpvWidget->controller()->setProperty(QStringLiteral("sid"),
|
m_mpvController->setProperty(QStringLiteral("sid"),
|
||||||
m_selectedSubtitleTrackId);
|
m_selectedSubtitleTrackId);
|
||||||
} else {
|
} else {
|
||||||
m_mpvWidget->controller()->setProperty(QStringLiteral("sid"),
|
m_mpvController->setProperty(QStringLiteral("sid"),
|
||||||
QStringLiteral("no"));
|
QStringLiteral("no"));
|
||||||
}
|
}
|
||||||
clearDanmakuMotionStabilityProfile();
|
clearDanmakuMotionStabilityProfile();
|
||||||
SubtitleStyleUtils::applyToController(m_mpvWidget->controller(), false);
|
SubtitleStyleUtils::applyToController(m_mpvController, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
QCoro::Task<void> PlayerDanmakuController::loadDanmakuTask(quint64 requestId,
|
QCoro::Task<void> PlayerDanmakuController::loadDanmakuTask(quint64 requestId,
|
||||||
|
|||||||
@@ -10,18 +10,21 @@
|
|||||||
#include <qcorotask.h>
|
#include <qcorotask.h>
|
||||||
|
|
||||||
class QEmbyCore;
|
class QEmbyCore;
|
||||||
class MpvWidget;
|
class MpvController;
|
||||||
class NativeDanmakuOverlay;
|
class IDanmakuSurface;
|
||||||
|
|
||||||
class PlayerDanmakuController : public QObject
|
class PlayerDanmakuController : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit PlayerDanmakuController(QEmbyCore *core,
|
explicit PlayerDanmakuController(QEmbyCore *core,
|
||||||
MpvWidget *mpvWidget,
|
MpvController *mpvController,
|
||||||
NativeDanmakuOverlay *nativeDanmakuOverlay,
|
IDanmakuSurface *danmakuSurface,
|
||||||
QObject *parent = nullptr);
|
QObject *parent = nullptr);
|
||||||
|
|
||||||
|
// Optional late-bind for the QML surface (constructed after the controller).
|
||||||
|
void setDanmakuSurface(IDanmakuSurface *surface);
|
||||||
|
|
||||||
void setPlaybackContext(const PlayerLaunchContext &context);
|
void setPlaybackContext(const PlayerLaunchContext &context);
|
||||||
void clearPlaybackContext();
|
void clearPlaybackContext();
|
||||||
|
|
||||||
@@ -78,8 +81,8 @@ private:
|
|||||||
bool saveAsManualMatch);
|
bool saveAsManualMatch);
|
||||||
|
|
||||||
QPointer<QEmbyCore> m_core;
|
QPointer<QEmbyCore> m_core;
|
||||||
QPointer<MpvWidget> m_mpvWidget;
|
QPointer<MpvController> m_mpvController;
|
||||||
QPointer<NativeDanmakuOverlay> m_nativeDanmakuOverlay;
|
IDanmakuSurface *m_danmakuSurface = nullptr;
|
||||||
PlayerLaunchContext m_launchContext;
|
PlayerLaunchContext m_launchContext;
|
||||||
DanmakuMediaContext m_mediaContext;
|
DanmakuMediaContext m_mediaContext;
|
||||||
QString m_assFilePath;
|
QString m_assFilePath;
|
||||||
|
|||||||
@@ -1,825 +0,0 @@
|
|||||||
#include "playerlongpresshandler.h"
|
|
||||||
#include <QAbstractAnimation>
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QEasingCurve>
|
|
||||||
#include <QGraphicsOpacityEffect>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QIcon>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QPropertyAnimation>
|
|
||||||
#include <QStyle>
|
|
||||||
#include <QTimer>
|
|
||||||
#include <QWidget>
|
|
||||||
#include <cmath>
|
|
||||||
#include <config/config_keys.h>
|
|
||||||
#include <config/configstore.h>
|
|
||||||
#include "../tokens/AnimationTokens.h"
|
|
||||||
#include "../utils/motionprefs.h"
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
void stopPropertyAnimationSafely(QPropertyAnimation *animation)
|
|
||||||
{
|
|
||||||
if (!animation || !animation->targetObject() ||
|
|
||||||
animation->state() == QAbstractAnimation::Stopped)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
animation->stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerLongPressHandler::PlayerLongPressHandler(QWidget *parent)
|
|
||||||
: QObject(parent), m_parentWidget(parent)
|
|
||||||
{
|
|
||||||
|
|
||||||
m_keyLongPressTimer = new QTimer(this);
|
|
||||||
connect(m_keyLongPressTimer, &QTimer::timeout, this,
|
|
||||||
&PlayerLongPressHandler::onKeyLongPressTimeout);
|
|
||||||
|
|
||||||
|
|
||||||
m_mouseEdgeTimer = new QTimer(this);
|
|
||||||
connect(m_mouseEdgeTimer, &QTimer::timeout, this,
|
|
||||||
&PlayerLongPressHandler::onMouseEdgeLongPressTimeout);
|
|
||||||
|
|
||||||
|
|
||||||
m_pulseTimer = new QTimer(this);
|
|
||||||
m_pulseTimer->setInterval(260);
|
|
||||||
connect(m_pulseTimer, &QTimer::timeout, this,
|
|
||||||
&PlayerLongPressHandler::updateIndicatorPulse);
|
|
||||||
|
|
||||||
|
|
||||||
m_leftIndicator = createIndicator(
|
|
||||||
QStringLiteral(":/svg/player/fast-rewind.svg"),
|
|
||||||
m_leftIndicatorIcon, m_leftIndicatorText,
|
|
||||||
m_leftIndicatorOpacity, m_leftIndicatorOpacityAnim,
|
|
||||||
m_leftIndicatorGeometryAnim, m_leftIndicatorIconAnim);
|
|
||||||
m_rightIndicator = createIndicator(
|
|
||||||
QStringLiteral(":/svg/player/fast-forward.svg"),
|
|
||||||
m_rightIndicatorIcon, m_rightIndicatorText,
|
|
||||||
m_rightIndicatorOpacity, m_rightIndicatorOpacityAnim,
|
|
||||||
m_rightIndicatorGeometryAnim, m_rightIndicatorIconAnim);
|
|
||||||
}
|
|
||||||
|
|
||||||
QWidget *PlayerLongPressHandler::createIndicator(
|
|
||||||
const QString &iconPath, QLabel *&iconLabel, QLabel *&textLabel,
|
|
||||||
QGraphicsOpacityEffect *&opacityEffect,
|
|
||||||
QPropertyAnimation *&opacityAnim,
|
|
||||||
QPropertyAnimation *&geometryAnim,
|
|
||||||
QPropertyAnimation *&iconAnim)
|
|
||||||
{
|
|
||||||
auto *overlay = new QWidget(m_parentWidget);
|
|
||||||
overlay->setObjectName("playerMouseEdgeIndicator");
|
|
||||||
overlay->setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
|
||||||
overlay->setAttribute(Qt::WA_TranslucentBackground, true);
|
|
||||||
overlay->setProperty("gestureActive", false);
|
|
||||||
overlay->setProperty("indicatorVisible", false);
|
|
||||||
overlay->hide();
|
|
||||||
|
|
||||||
auto *layout = new QHBoxLayout(overlay);
|
|
||||||
layout->setContentsMargins(18, 12, 18, 12);
|
|
||||||
layout->setSpacing(12);
|
|
||||||
layout->setAlignment(Qt::AlignCenter);
|
|
||||||
|
|
||||||
iconLabel = new QLabel(overlay);
|
|
||||||
iconLabel->setObjectName("playerMouseEdgeIndicatorIcon");
|
|
||||||
iconLabel->setFixedSize(18, 18);
|
|
||||||
iconLabel->setAlignment(Qt::AlignCenter);
|
|
||||||
iconLabel->setPixmap(QIcon(iconPath).pixmap(18, 18));
|
|
||||||
layout->addWidget(iconLabel, 0, Qt::AlignCenter);
|
|
||||||
|
|
||||||
textLabel = new QLabel(overlay);
|
|
||||||
textLabel->setObjectName("playerMouseEdgeIndicatorText");
|
|
||||||
textLabel->setAlignment(Qt::AlignCenter);
|
|
||||||
textLabel->hide();
|
|
||||||
layout->addWidget(textLabel, 0, Qt::AlignCenter);
|
|
||||||
|
|
||||||
opacityEffect = new QGraphicsOpacityEffect(overlay);
|
|
||||||
opacityEffect->setOpacity(0.0);
|
|
||||||
overlay->setGraphicsEffect(opacityEffect);
|
|
||||||
|
|
||||||
opacityAnim = new QPropertyAnimation(opacityEffect, "opacity", this);
|
|
||||||
opacityAnim->setEasingCurve(AnimationTokens::Curve::enter());
|
|
||||||
|
|
||||||
geometryAnim = new QPropertyAnimation(overlay, "geometry", this);
|
|
||||||
geometryAnim->setEasingCurve(AnimationTokens::Curve::enter());
|
|
||||||
|
|
||||||
iconAnim = new QPropertyAnimation(iconLabel, "pos", this);
|
|
||||||
iconAnim->setDuration(AnimationTokens::DurationMs::kNormal);
|
|
||||||
iconAnim->setEasingCurve(AnimationTokens::Curve::oscillate());
|
|
||||||
|
|
||||||
connect(opacityAnim, &QPropertyAnimation::finished, overlay,
|
|
||||||
[overlay, opacityEffect]()
|
|
||||||
{
|
|
||||||
if (!overlay->property("indicatorVisible").toBool() &&
|
|
||||||
opacityEffect && opacityEffect->opacity() <= 0.01)
|
|
||||||
{
|
|
||||||
overlay->hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return overlay;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
QString PlayerLongPressHandler::longPressMode() const
|
|
||||||
{
|
|
||||||
const QString mode =
|
|
||||||
ConfigStore::instance()->get<QString>(ConfigKeys::PlayerLongPressMode, "seek");
|
|
||||||
return mode == QStringLiteral("speed") ? mode : QStringLiteral("seek");
|
|
||||||
}
|
|
||||||
|
|
||||||
int PlayerLongPressHandler::longPressTriggerMs() const
|
|
||||||
{
|
|
||||||
const int triggerMs =
|
|
||||||
ConfigStore::instance()->get<QString>(ConfigKeys::PlayerLongPressTriggerMs, "500").toInt();
|
|
||||||
return triggerMs > 0 ? triggerMs : 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
double PlayerLongPressHandler::nextPlaybackRate(double currentRate) const
|
|
||||||
{
|
|
||||||
static const QList<double> kSpeedSteps = {2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0};
|
|
||||||
for (double rate : kSpeedSteps)
|
|
||||||
{
|
|
||||||
if (rate > currentRate + 0.001)
|
|
||||||
{
|
|
||||||
return rate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return kSpeedSteps.constLast();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PlayerLongPressHandler::canStartPropertyAnimation(const QPropertyAnimation *animation) const
|
|
||||||
{
|
|
||||||
return animation && animation->targetObject() && !MotionPrefs::reduceMotion();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PlayerLongPressHandler::isMouseEdgeLongPressEnabled() const
|
|
||||||
{
|
|
||||||
return ConfigStore::instance()->get<bool>(ConfigKeys::PlayerMouseEdgeLongPress, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
QString PlayerLongPressHandler::formatSpeedText(double speed)
|
|
||||||
{
|
|
||||||
QString text = QString::number(speed, 'f', 2);
|
|
||||||
while (text.contains('.') && text.endsWith('0'))
|
|
||||||
{
|
|
||||||
text.chop(1);
|
|
||||||
}
|
|
||||||
if (text.endsWith('.'))
|
|
||||||
{
|
|
||||||
text.append('0');
|
|
||||||
}
|
|
||||||
return text + "X";
|
|
||||||
}
|
|
||||||
|
|
||||||
QString PlayerLongPressHandler::formatLongPressSeekText(double seconds)
|
|
||||||
{
|
|
||||||
QString text = QString::number(seconds, 'f', seconds >= 10.0 ? 0 : 1);
|
|
||||||
while (text.contains('.') && text.endsWith('0'))
|
|
||||||
{
|
|
||||||
text.chop(1);
|
|
||||||
}
|
|
||||||
if (text.endsWith('.'))
|
|
||||||
{
|
|
||||||
text.chop(1);
|
|
||||||
}
|
|
||||||
return text + "s";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void PlayerLongPressHandler::startKeyLongPress(int direction, bool silent)
|
|
||||||
{
|
|
||||||
m_teardown = false;
|
|
||||||
stopKeyLongPress(false);
|
|
||||||
|
|
||||||
m_keyDirection = direction;
|
|
||||||
m_keySeekRate = 2.0;
|
|
||||||
m_keyRestoreSpeed = 1.0;
|
|
||||||
m_keyCurrentSpeed = 1.0;
|
|
||||||
m_keySpeedTick = 0;
|
|
||||||
m_keyTriggered = false;
|
|
||||||
m_keySilent = silent;
|
|
||||||
m_keySpeedActive = false;
|
|
||||||
m_keyActiveMode = longPressMode();
|
|
||||||
|
|
||||||
const double step =
|
|
||||||
ConfigStore::instance()->get<QString>(ConfigKeys::PlayerSeekStep, "10").toDouble();
|
|
||||||
const bool longPressEnabled =
|
|
||||||
ConfigStore::instance()->get<bool>(ConfigKeys::PlayerLongPressSeek, true);
|
|
||||||
|
|
||||||
if (!longPressEnabled)
|
|
||||||
{
|
|
||||||
Q_EMIT seekRequested(direction * step, silent);
|
|
||||||
m_keyDirection = 0;
|
|
||||||
m_keySilent = false;
|
|
||||||
m_keyActiveMode.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_keyActiveMode == QStringLiteral("seek"))
|
|
||||||
{
|
|
||||||
Q_EMIT seekRequested(direction * step, silent);
|
|
||||||
}
|
|
||||||
|
|
||||||
qDebug() << "[PlayerLongPressHandler] Direction key pressed"
|
|
||||||
<< "| direction:" << direction
|
|
||||||
<< "| mode:" << m_keyActiveMode
|
|
||||||
<< "| triggerMs:" << longPressTriggerMs();
|
|
||||||
|
|
||||||
m_keyLongPressTimer->setInterval(longPressTriggerMs());
|
|
||||||
m_keyLongPressTimer->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerLongPressHandler::stopKeyLongPress(bool applyShortPressFallback)
|
|
||||||
{
|
|
||||||
if (m_keyLongPressTimer)
|
|
||||||
{
|
|
||||||
m_keyLongPressTimer->stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_keyDirection == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_keyActiveMode == QStringLiteral("speed"))
|
|
||||||
{
|
|
||||||
if (!m_keySpeedActive && applyShortPressFallback)
|
|
||||||
{
|
|
||||||
const double step = ConfigStore::instance()
|
|
||||||
->get<QString>(ConfigKeys::PlayerSeekStep, "10")
|
|
||||||
.toDouble();
|
|
||||||
Q_EMIT seekRequested(m_keyDirection * step, m_keySilent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_keyDirection = 0;
|
|
||||||
m_keySeekRate = 2.0;
|
|
||||||
m_keyRestoreSpeed = 1.0;
|
|
||||||
m_keyCurrentSpeed = 1.0;
|
|
||||||
m_keySpeedTick = 0;
|
|
||||||
m_keyTriggered = false;
|
|
||||||
m_keySilent = false;
|
|
||||||
m_keySpeedActive = false;
|
|
||||||
m_keyActiveMode.clear();
|
|
||||||
updateIndicators();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerLongPressHandler::onKeyLongPressTimeout()
|
|
||||||
{
|
|
||||||
if (m_keyDirection == 0 || m_keyActiveMode.isEmpty())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_keyActiveMode == QStringLiteral("speed"))
|
|
||||||
{
|
|
||||||
if (!m_keyTriggered)
|
|
||||||
{
|
|
||||||
m_keyTriggered = true;
|
|
||||||
m_keySpeedActive = true;
|
|
||||||
m_keyCurrentSpeed = nextPlaybackRate(1.0);
|
|
||||||
m_keySpeedTick = 0;
|
|
||||||
|
|
||||||
const QString speedText = formatSpeedText(m_keyCurrentSpeed);
|
|
||||||
updateIndicators();
|
|
||||||
Q_EMIT toastRequested(tr("Speed: %1X").arg(speedText.chopped(1)));
|
|
||||||
|
|
||||||
qDebug() << "[PlayerLongPressHandler] Long press speed activated"
|
|
||||||
<< "| direction:" << m_keyDirection
|
|
||||||
<< "| targetSpeed:" << m_keyCurrentSpeed;
|
|
||||||
}
|
|
||||||
else if (m_keySpeedTick > 0 && m_keySpeedTick % 2 == 0)
|
|
||||||
{
|
|
||||||
const double nextSpeed = nextPlaybackRate(m_keyCurrentSpeed);
|
|
||||||
if (std::abs(nextSpeed - m_keyCurrentSpeed) > 0.001)
|
|
||||||
{
|
|
||||||
m_keyCurrentSpeed = nextSpeed;
|
|
||||||
const QString speedText = formatSpeedText(m_keyCurrentSpeed);
|
|
||||||
updateIndicators();
|
|
||||||
Q_EMIT toastRequested(tr("Speed: %1X").arg(speedText.chopped(1)));
|
|
||||||
qDebug() << "[PlayerLongPressHandler] Long press speed increased"
|
|
||||||
<< "| direction:" << m_keyDirection
|
|
||||||
<< "| targetSpeed:" << m_keyCurrentSpeed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const double deltaSeconds = m_keyDirection * m_keyCurrentSpeed * 0.25;
|
|
||||||
Q_EMIT seekRequested(deltaSeconds, true);
|
|
||||||
++m_keySpeedTick;
|
|
||||||
m_keyLongPressTimer->setInterval(250);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool wasTriggered = m_keyTriggered;
|
|
||||||
m_keyTriggered = true;
|
|
||||||
m_keyLongPressTimer->setInterval(100);
|
|
||||||
Q_EMIT seekRequested(m_keyDirection * m_keySeekRate, m_keySilent);
|
|
||||||
updateIndicators();
|
|
||||||
|
|
||||||
if (!wasTriggered)
|
|
||||||
{
|
|
||||||
qDebug() << "[PlayerLongPressHandler] Long press seek activated"
|
|
||||||
<< "| direction:" << m_keyDirection;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_keySeekRate *= 1.15;
|
|
||||||
if (m_keySeekRate > 30.0)
|
|
||||||
{
|
|
||||||
m_keySeekRate = 30.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void PlayerLongPressHandler::startMouseEdgeLongPress(int direction)
|
|
||||||
{
|
|
||||||
m_teardown = false;
|
|
||||||
stopMouseEdgeLongPress();
|
|
||||||
|
|
||||||
if (!isMouseEdgeLongPressEnabled())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_mouseEdgeDirection = direction;
|
|
||||||
m_mouseEdgeTick = 0;
|
|
||||||
m_mouseEdgeSpeed = 1.0;
|
|
||||||
m_mouseEdgeActive = false;
|
|
||||||
|
|
||||||
qDebug() << "[PlayerLongPressHandler] Mouse edge long press armed"
|
|
||||||
<< "| direction:" << direction
|
|
||||||
<< "| triggerMs:" << longPressTriggerMs();
|
|
||||||
|
|
||||||
if (m_mouseEdgeTimer)
|
|
||||||
{
|
|
||||||
m_mouseEdgeTimer->setInterval(longPressTriggerMs());
|
|
||||||
m_mouseEdgeTimer->start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PlayerLongPressHandler::stopMouseEdgeLongPress()
|
|
||||||
{
|
|
||||||
if (m_mouseEdgeTimer)
|
|
||||||
{
|
|
||||||
m_mouseEdgeTimer->stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool wasActive = m_mouseEdgeActive;
|
|
||||||
if (wasActive)
|
|
||||||
{
|
|
||||||
qDebug() << "[PlayerLongPressHandler] Mouse edge long press stopped"
|
|
||||||
<< "| direction:" << m_mouseEdgeDirection
|
|
||||||
<< "| speed:" << m_mouseEdgeSpeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_mouseEdgeDirection = 0;
|
|
||||||
m_mouseEdgeTick = 0;
|
|
||||||
m_mouseEdgeSpeed = 1.0;
|
|
||||||
m_mouseEdgeActive = false;
|
|
||||||
updateIndicators();
|
|
||||||
return wasActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerLongPressHandler::onMouseEdgeLongPressTimeout()
|
|
||||||
{
|
|
||||||
if (m_mouseEdgeDirection == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_mouseEdgeActive)
|
|
||||||
{
|
|
||||||
m_mouseEdgeActive = true;
|
|
||||||
m_mouseEdgeSpeed = nextPlaybackRate(1.0);
|
|
||||||
updateIndicators();
|
|
||||||
qDebug() << "[PlayerLongPressHandler] Mouse edge long press activated"
|
|
||||||
<< "| direction:" << m_mouseEdgeDirection
|
|
||||||
<< "| speed:" << m_mouseEdgeSpeed;
|
|
||||||
}
|
|
||||||
else if (m_mouseEdgeTick > 0 && m_mouseEdgeTick % 2 == 0)
|
|
||||||
{
|
|
||||||
const double nextSpeed = nextPlaybackRate(m_mouseEdgeSpeed);
|
|
||||||
if (std::abs(nextSpeed - m_mouseEdgeSpeed) > 0.001)
|
|
||||||
{
|
|
||||||
m_mouseEdgeSpeed = nextSpeed;
|
|
||||||
updateIndicators();
|
|
||||||
qDebug() << "[PlayerLongPressHandler] Mouse edge long press accelerated"
|
|
||||||
<< "| direction:" << m_mouseEdgeDirection
|
|
||||||
<< "| speed:" << m_mouseEdgeSpeed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const double deltaSeconds = m_mouseEdgeDirection * m_mouseEdgeSpeed * 0.25;
|
|
||||||
Q_EMIT seekRequested(deltaSeconds, true);
|
|
||||||
++m_mouseEdgeTick;
|
|
||||||
|
|
||||||
if (m_mouseEdgeTimer)
|
|
||||||
{
|
|
||||||
m_mouseEdgeTimer->setInterval(250);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
bool PlayerLongPressHandler::isMouseEdgeActive() const
|
|
||||||
{
|
|
||||||
return m_mouseEdgeActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
int PlayerLongPressHandler::mouseEdgeLongPressDirection() const
|
|
||||||
{
|
|
||||||
return m_mouseEdgeDirection;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PlayerLongPressHandler::isKeyLongPressTriggered() const
|
|
||||||
{
|
|
||||||
return m_keyTriggered;
|
|
||||||
}
|
|
||||||
|
|
||||||
int PlayerLongPressHandler::keyLongPressDirection() const
|
|
||||||
{
|
|
||||||
return m_keyDirection;
|
|
||||||
}
|
|
||||||
|
|
||||||
int PlayerLongPressHandler::activeLongPressDirection() const
|
|
||||||
{
|
|
||||||
if (m_mouseEdgeActive && m_mouseEdgeDirection != 0)
|
|
||||||
{
|
|
||||||
return m_mouseEdgeDirection;
|
|
||||||
}
|
|
||||||
if (m_keyTriggered && m_keyDirection != 0)
|
|
||||||
{
|
|
||||||
return m_keyDirection;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerLongPressHandler::setTeardown(bool teardown)
|
|
||||||
{
|
|
||||||
m_teardown = teardown;
|
|
||||||
if (!m_teardown)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_keyLongPressTimer)
|
|
||||||
{
|
|
||||||
m_keyLongPressTimer->stop();
|
|
||||||
}
|
|
||||||
if (m_mouseEdgeTimer)
|
|
||||||
{
|
|
||||||
m_mouseEdgeTimer->stop();
|
|
||||||
}
|
|
||||||
stopIndicatorAnimations();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
QRect PlayerLongPressHandler::indicatorGeometry(int direction, bool active) const
|
|
||||||
{
|
|
||||||
const int w = m_parentWidget ? m_parentWidget->width() : 800;
|
|
||||||
const int h = m_parentWidget ? m_parentWidget->height() : 600;
|
|
||||||
const int indicatorWidth = active ? 128 : 60;
|
|
||||||
const int indicatorHeight = 54;
|
|
||||||
const int indicatorMargin = 28;
|
|
||||||
const int indicatorY = qMax(m_topHudHeight + 24, (h - indicatorHeight) / 2);
|
|
||||||
const int indicatorX = direction < 0
|
|
||||||
? indicatorMargin
|
|
||||||
: w - indicatorWidth - indicatorMargin;
|
|
||||||
return QRect(indicatorX, indicatorY, indicatorWidth, indicatorHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerLongPressHandler::animateIndicator(
|
|
||||||
QWidget *overlay, int direction, bool active, bool visible)
|
|
||||||
{
|
|
||||||
if (!overlay)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QGraphicsOpacityEffect *opacityEffect =
|
|
||||||
direction < 0 ? m_leftIndicatorOpacity : m_rightIndicatorOpacity;
|
|
||||||
QPropertyAnimation *opacityAnim =
|
|
||||||
direction < 0 ? m_leftIndicatorOpacityAnim : m_rightIndicatorOpacityAnim;
|
|
||||||
QPropertyAnimation *geometryAnim =
|
|
||||||
direction < 0 ? m_leftIndicatorGeometryAnim : m_rightIndicatorGeometryAnim;
|
|
||||||
|
|
||||||
const QRect targetGeometry = indicatorGeometry(direction, active);
|
|
||||||
if (!opacityEffect || !opacityAnim || !geometryAnim)
|
|
||||||
{
|
|
||||||
overlay->setGeometry(targetGeometry);
|
|
||||||
overlay->setVisible(visible);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!opacityAnim->targetObject() || !geometryAnim->targetObject() || m_teardown)
|
|
||||||
{
|
|
||||||
overlay->setProperty("indicatorVisible", visible);
|
|
||||||
overlay->setGeometry(targetGeometry);
|
|
||||||
opacityEffect->setOpacity(visible ? (active ? 1.0 : 0.62) : 0.0);
|
|
||||||
overlay->setVisible(visible);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stopPropertyAnimationSafely(opacityAnim);
|
|
||||||
stopPropertyAnimationSafely(geometryAnim);
|
|
||||||
|
|
||||||
if (visible)
|
|
||||||
{
|
|
||||||
const bool wasVisible = overlay->isVisible();
|
|
||||||
const QRect startGeometry = wasVisible
|
|
||||||
? overlay->geometry()
|
|
||||||
: targetGeometry.translated(
|
|
||||||
direction < 0 ? -14 : 14, 0);
|
|
||||||
const qreal startOpacity = wasVisible ? opacityEffect->opacity() : 0.0;
|
|
||||||
const qreal targetOpacity = active ? 1.0 : 0.62;
|
|
||||||
|
|
||||||
overlay->setProperty("indicatorVisible", true);
|
|
||||||
overlay->setGeometry(startGeometry);
|
|
||||||
overlay->show();
|
|
||||||
overlay->raise();
|
|
||||||
opacityEffect->setOpacity(startOpacity);
|
|
||||||
|
|
||||||
opacityAnim->setDuration(wasVisible ? AnimationTokens::DurationMs::kMicro
|
|
||||||
: AnimationTokens::DurationMs::kFast);
|
|
||||||
opacityAnim->setStartValue(startOpacity);
|
|
||||||
opacityAnim->setEndValue(targetOpacity);
|
|
||||||
|
|
||||||
// Geometry lags the opacity slightly (140/200 vs 120/180) so the pill
|
|
||||||
// reads as sliding into place — tuned choreography, intentionally off
|
|
||||||
// the shared duration scale.
|
|
||||||
geometryAnim->setDuration(wasVisible ? 140 : 200);
|
|
||||||
geometryAnim->setStartValue(startGeometry);
|
|
||||||
geometryAnim->setEndValue(targetGeometry);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!overlay->isVisible())
|
|
||||||
{
|
|
||||||
opacityEffect->setOpacity(0.0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QRect currentGeometry = overlay->geometry();
|
|
||||||
overlay->setProperty("indicatorVisible", false);
|
|
||||||
|
|
||||||
opacityAnim->setDuration(AnimationTokens::DurationMs::kMicro);
|
|
||||||
opacityAnim->setStartValue(opacityEffect->opacity());
|
|
||||||
opacityAnim->setEndValue(0.0);
|
|
||||||
|
|
||||||
geometryAnim->setDuration(AnimationTokens::DurationMs::kMicro);
|
|
||||||
geometryAnim->setStartValue(currentGeometry);
|
|
||||||
geometryAnim->setEndValue(
|
|
||||||
currentGeometry.translated(direction < 0 ? -10 : 10, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canStartPropertyAnimation(opacityAnim))
|
|
||||||
{
|
|
||||||
opacityAnim->start();
|
|
||||||
}
|
|
||||||
else if (opacityEffect)
|
|
||||||
{
|
|
||||||
opacityEffect->setOpacity(visible ? (active ? 1.0 : 0.62) : 0.0);
|
|
||||||
if (!visible)
|
|
||||||
{
|
|
||||||
// The animated path hides via the opacityAnim finished handler;
|
|
||||||
// when snapping we must hide directly.
|
|
||||||
overlay->hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canStartPropertyAnimation(geometryAnim))
|
|
||||||
{
|
|
||||||
geometryAnim->start();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
overlay->setGeometry(visible ? targetGeometry
|
|
||||||
: overlay->geometry().translated(
|
|
||||||
direction < 0 ? -10 : 10, 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerLongPressHandler::animateIndicatorIcon(
|
|
||||||
QLabel *iconLabel, int direction, bool active, bool immediate)
|
|
||||||
{
|
|
||||||
if (!iconLabel)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPropertyAnimation *iconAnim =
|
|
||||||
direction < 0 ? m_leftIndicatorIconAnim : m_rightIndicatorIconAnim;
|
|
||||||
if (!iconAnim)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QPoint basePos = iconLabel->property("basePos").toPoint();
|
|
||||||
const int offset = (active && m_pulseForward)
|
|
||||||
? (direction < 0 ? -4 : 4)
|
|
||||||
: 0;
|
|
||||||
const QPoint targetPos = basePos + QPoint(offset, 0);
|
|
||||||
|
|
||||||
if (immediate || !iconAnim->targetObject() || m_teardown)
|
|
||||||
{
|
|
||||||
iconLabel->move(targetPos);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stopPropertyAnimationSafely(iconAnim);
|
|
||||||
iconAnim->setStartValue(iconLabel->pos());
|
|
||||||
iconAnim->setEndValue(targetPos);
|
|
||||||
if (canStartPropertyAnimation(iconAnim))
|
|
||||||
{
|
|
||||||
iconAnim->start();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
iconLabel->move(targetPos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerLongPressHandler::setIndicatorState(
|
|
||||||
QWidget *overlay, QLabel *iconLabel, QLabel *textLabel, int direction,
|
|
||||||
bool active, const QString &speedText)
|
|
||||||
{
|
|
||||||
if (!overlay || !iconLabel || !textLabel)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
overlay->setProperty("gestureActive", active);
|
|
||||||
overlay->style()->unpolish(overlay);
|
|
||||||
overlay->style()->polish(overlay);
|
|
||||||
overlay->update();
|
|
||||||
|
|
||||||
if (speedText.isEmpty())
|
|
||||||
{
|
|
||||||
textLabel->clear();
|
|
||||||
textLabel->hide();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
textLabel->setText(speedText);
|
|
||||||
textLabel->show();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overlay->layout())
|
|
||||||
{
|
|
||||||
overlay->layout()->activate();
|
|
||||||
}
|
|
||||||
|
|
||||||
iconLabel->setProperty("basePos", iconLabel->pos());
|
|
||||||
animateIndicatorIcon(iconLabel, direction, active, true);
|
|
||||||
animateIndicator(overlay, direction, active, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerLongPressHandler::updateIndicatorPulse()
|
|
||||||
{
|
|
||||||
if (m_teardown)
|
|
||||||
{
|
|
||||||
stopIndicatorAnimations();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int activeDirection = activeLongPressDirection();
|
|
||||||
if (activeDirection == 0)
|
|
||||||
{
|
|
||||||
if (m_pulseTimer)
|
|
||||||
{
|
|
||||||
m_pulseTimer->stop();
|
|
||||||
}
|
|
||||||
m_pulseForward = false;
|
|
||||||
animateIndicatorIcon(m_leftIndicatorIcon, -1, false, true);
|
|
||||||
animateIndicatorIcon(m_rightIndicatorIcon, 1, false, true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_pulseForward = !m_pulseForward;
|
|
||||||
animateIndicatorIcon(m_leftIndicatorIcon, -1, activeDirection < 0);
|
|
||||||
animateIndicatorIcon(m_rightIndicatorIcon, 1, activeDirection > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerLongPressHandler::updateIndicators()
|
|
||||||
{
|
|
||||||
if (m_teardown)
|
|
||||||
{
|
|
||||||
stopIndicatorAnimations();
|
|
||||||
|
|
||||||
if (m_leftIndicator)
|
|
||||||
{
|
|
||||||
m_leftIndicator->hide();
|
|
||||||
}
|
|
||||||
if (m_rightIndicator)
|
|
||||||
{
|
|
||||||
m_rightIndicator->hide();
|
|
||||||
}
|
|
||||||
animateIndicatorIcon(m_leftIndicatorIcon, -1, false, true);
|
|
||||||
animateIndicatorIcon(m_rightIndicatorIcon, 1, false, true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int activeDirection = activeLongPressDirection();
|
|
||||||
if (activeDirection == 0)
|
|
||||||
{
|
|
||||||
if (m_leftIndicator)
|
|
||||||
{
|
|
||||||
m_leftIndicator->setProperty("gestureActive", false);
|
|
||||||
m_leftIndicator->style()->unpolish(m_leftIndicator);
|
|
||||||
m_leftIndicator->style()->polish(m_leftIndicator);
|
|
||||||
animateIndicator(m_leftIndicator, -1, false, false);
|
|
||||||
}
|
|
||||||
if (m_rightIndicator)
|
|
||||||
{
|
|
||||||
m_rightIndicator->setProperty("gestureActive", false);
|
|
||||||
m_rightIndicator->style()->unpolish(m_rightIndicator);
|
|
||||||
m_rightIndicator->style()->polish(m_rightIndicator);
|
|
||||||
animateIndicator(m_rightIndicator, 1, false, false);
|
|
||||||
}
|
|
||||||
updateIndicatorPulse();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool fromMouse = m_mouseEdgeActive && m_mouseEdgeDirection != 0;
|
|
||||||
const QString indicatorText =
|
|
||||||
fromMouse
|
|
||||||
? formatSpeedText(m_mouseEdgeSpeed)
|
|
||||||
: (m_keyActiveMode == QStringLiteral("speed")
|
|
||||||
? formatSpeedText(m_keyCurrentSpeed)
|
|
||||||
: formatLongPressSeekText(m_keySeekRate));
|
|
||||||
const bool leftActive = activeDirection < 0;
|
|
||||||
const bool rightActive = activeDirection > 0;
|
|
||||||
|
|
||||||
setIndicatorState(
|
|
||||||
m_leftIndicator, m_leftIndicatorIcon,
|
|
||||||
m_leftIndicatorText, -1, leftActive,
|
|
||||||
leftActive ? indicatorText : QString());
|
|
||||||
setIndicatorState(
|
|
||||||
m_rightIndicator, m_rightIndicatorIcon,
|
|
||||||
m_rightIndicatorText, 1, rightActive,
|
|
||||||
rightActive ? indicatorText : QString());
|
|
||||||
|
|
||||||
if (m_pulseTimer && !m_pulseTimer->isActive() && !MotionPrefs::reduceMotion())
|
|
||||||
{
|
|
||||||
m_pulseForward = false;
|
|
||||||
updateIndicatorPulse();
|
|
||||||
m_pulseTimer->start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerLongPressHandler::stopIndicatorAnimations()
|
|
||||||
{
|
|
||||||
if (m_pulseTimer)
|
|
||||||
{
|
|
||||||
m_pulseTimer->stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
const QList<QPropertyAnimation *> animations = {
|
|
||||||
m_leftIndicatorOpacityAnim, m_rightIndicatorOpacityAnim,
|
|
||||||
m_leftIndicatorGeometryAnim, m_rightIndicatorGeometryAnim,
|
|
||||||
m_leftIndicatorIconAnim, m_rightIndicatorIconAnim};
|
|
||||||
for (QPropertyAnimation *animation : animations)
|
|
||||||
{
|
|
||||||
stopPropertyAnimationSafely(animation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerLongPressHandler::stopAllAnimations()
|
|
||||||
{
|
|
||||||
stopIndicatorAnimations();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerLongPressHandler::updateGeometry(int parentWidth, int parentHeight, int topHudHeight)
|
|
||||||
{
|
|
||||||
Q_UNUSED(parentHeight);
|
|
||||||
m_topHudHeight = topHudHeight;
|
|
||||||
|
|
||||||
auto syncGeometry = [this](QWidget *overlay, QPropertyAnimation *animation, int direction)
|
|
||||||
{
|
|
||||||
if (!overlay)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QRect targetGeometry = indicatorGeometry(
|
|
||||||
direction, overlay->property("gestureActive").toBool());
|
|
||||||
if (animation && animation->state() == QAbstractAnimation::Running)
|
|
||||||
{
|
|
||||||
animation->setEndValue(targetGeometry);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
overlay->setGeometry(targetGeometry);
|
|
||||||
};
|
|
||||||
|
|
||||||
syncGeometry(m_leftIndicator, m_leftIndicatorGeometryAnim, -1);
|
|
||||||
syncGeometry(m_rightIndicator, m_rightIndicatorGeometryAnim, 1);
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
#ifndef PLAYERLONGPRESSHANDLER_H
|
|
||||||
#define PLAYERLONGPRESSHANDLER_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QPoint>
|
|
||||||
#include <QRect>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
class QWidget;
|
|
||||||
class QLabel;
|
|
||||||
class QGraphicsOpacityEffect;
|
|
||||||
class QPropertyAnimation;
|
|
||||||
class QTimer;
|
|
||||||
|
|
||||||
|
|
||||||
class PlayerLongPressHandler : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit PlayerLongPressHandler(QWidget *parent);
|
|
||||||
|
|
||||||
|
|
||||||
void startKeyLongPress(int direction, bool silent);
|
|
||||||
void stopKeyLongPress(bool applyShortPressFallback = true);
|
|
||||||
|
|
||||||
|
|
||||||
void startMouseEdgeLongPress(int direction);
|
|
||||||
bool stopMouseEdgeLongPress();
|
|
||||||
bool isMouseEdgeLongPressEnabled() const;
|
|
||||||
bool isMouseEdgeActive() const;
|
|
||||||
int mouseEdgeLongPressDirection() const;
|
|
||||||
|
|
||||||
|
|
||||||
void updateGeometry(int parentWidth, int parentHeight, int topHudHeight);
|
|
||||||
void setTeardown(bool teardown);
|
|
||||||
void stopAllAnimations();
|
|
||||||
|
|
||||||
|
|
||||||
int activeLongPressDirection() const;
|
|
||||||
bool isKeyLongPressTriggered() const;
|
|
||||||
int keyLongPressDirection() const;
|
|
||||||
|
|
||||||
|
|
||||||
static QString formatSpeedText(double speed);
|
|
||||||
static QString formatLongPressSeekText(double seconds);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void seekRequested(double delta, bool silent);
|
|
||||||
void toastRequested(const QString &msg);
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
QString longPressMode() const;
|
|
||||||
int longPressTriggerMs() const;
|
|
||||||
double nextPlaybackRate(double currentRate) const;
|
|
||||||
bool canStartPropertyAnimation(const QPropertyAnimation *animation) const;
|
|
||||||
|
|
||||||
|
|
||||||
void onKeyLongPressTimeout();
|
|
||||||
|
|
||||||
|
|
||||||
void onMouseEdgeLongPressTimeout();
|
|
||||||
|
|
||||||
|
|
||||||
void updateIndicators();
|
|
||||||
void updateIndicatorPulse();
|
|
||||||
QRect indicatorGeometry(int direction, bool active) const;
|
|
||||||
void animateIndicator(QWidget *overlay, int direction, bool active, bool visible);
|
|
||||||
void animateIndicatorIcon(QLabel *iconLabel, int direction, bool active, bool immediate = false);
|
|
||||||
void setIndicatorState(QWidget *overlay, QLabel *iconLabel, QLabel *textLabel,
|
|
||||||
int direction, bool active, const QString &speedText);
|
|
||||||
void stopIndicatorAnimations();
|
|
||||||
|
|
||||||
QWidget *createIndicator(const QString &iconPath, QLabel *&iconLabel, QLabel *&textLabel,
|
|
||||||
QGraphicsOpacityEffect *&opacityEffect,
|
|
||||||
QPropertyAnimation *&opacityAnim,
|
|
||||||
QPropertyAnimation *&geometryAnim,
|
|
||||||
QPropertyAnimation *&iconAnim);
|
|
||||||
|
|
||||||
|
|
||||||
QWidget *m_parentWidget = nullptr;
|
|
||||||
int m_topHudHeight = 0;
|
|
||||||
|
|
||||||
|
|
||||||
QTimer *m_keyLongPressTimer = nullptr;
|
|
||||||
int m_keyDirection = 0;
|
|
||||||
double m_keySeekRate = 2.0;
|
|
||||||
double m_keyRestoreSpeed = 1.0;
|
|
||||||
double m_keyCurrentSpeed = 1.0;
|
|
||||||
int m_keySpeedTick = 0;
|
|
||||||
bool m_keyTriggered = false;
|
|
||||||
bool m_keySilent = false;
|
|
||||||
bool m_keySpeedActive = false;
|
|
||||||
QString m_keyActiveMode;
|
|
||||||
|
|
||||||
|
|
||||||
QTimer *m_mouseEdgeTimer = nullptr;
|
|
||||||
int m_mouseEdgeDirection = 0;
|
|
||||||
int m_mouseEdgeTick = 0;
|
|
||||||
double m_mouseEdgeSpeed = 1.0;
|
|
||||||
bool m_mouseEdgeActive = false;
|
|
||||||
|
|
||||||
|
|
||||||
QWidget *m_leftIndicator = nullptr;
|
|
||||||
QWidget *m_rightIndicator = nullptr;
|
|
||||||
QLabel *m_leftIndicatorIcon = nullptr;
|
|
||||||
QLabel *m_rightIndicatorIcon = nullptr;
|
|
||||||
QLabel *m_leftIndicatorText = nullptr;
|
|
||||||
QLabel *m_rightIndicatorText = nullptr;
|
|
||||||
QGraphicsOpacityEffect *m_leftIndicatorOpacity = nullptr;
|
|
||||||
QGraphicsOpacityEffect *m_rightIndicatorOpacity = nullptr;
|
|
||||||
QPropertyAnimation *m_leftIndicatorOpacityAnim = nullptr;
|
|
||||||
QPropertyAnimation *m_rightIndicatorOpacityAnim = nullptr;
|
|
||||||
QPropertyAnimation *m_leftIndicatorGeometryAnim = nullptr;
|
|
||||||
QPropertyAnimation *m_rightIndicatorGeometryAnim = nullptr;
|
|
||||||
QPropertyAnimation *m_leftIndicatorIconAnim = nullptr;
|
|
||||||
QPropertyAnimation *m_rightIndicatorIconAnim = nullptr;
|
|
||||||
QTimer *m_pulseTimer = nullptr;
|
|
||||||
bool m_pulseForward = false;
|
|
||||||
bool m_teardown = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,416 +0,0 @@
|
|||||||
#include "playermediaswitcherpanel.h"
|
|
||||||
|
|
||||||
#include "horizontallistviewgallery.h"
|
|
||||||
#include "../utils/mediaitemutils.h"
|
|
||||||
|
|
||||||
#include <qembycore.h>
|
|
||||||
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QTimer>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
constexpr int kMovieGalleryHeight = 162;
|
|
||||||
constexpr int kSeasonGalleryHeight = 136;
|
|
||||||
constexpr int kEpisodeGalleryHeight = 110;
|
|
||||||
constexpr int kSpinnerSize = 28;
|
|
||||||
constexpr int kSpinnerContainerHeight = 60;
|
|
||||||
|
|
||||||
class SpinnerPainter : public QWidget {
|
|
||||||
public:
|
|
||||||
explicit SpinnerPainter(QWidget *parent = nullptr)
|
|
||||||
: QWidget(parent) {
|
|
||||||
setFixedSize(kSpinnerSize, kSpinnerSize);
|
|
||||||
setAttribute(Qt::WA_TranslucentBackground);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setAngle(qreal angle) {
|
|
||||||
m_angle = angle;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void paintEvent(QPaintEvent *) override {
|
|
||||||
QPainter p(this);
|
|
||||||
p.setRenderHint(QPainter::Antialiasing, true);
|
|
||||||
|
|
||||||
const qreal penWidth = 3.0;
|
|
||||||
const qreal margin = penWidth / 2.0;
|
|
||||||
const QRectF arcRect(margin, margin,
|
|
||||||
width() - penWidth, height() - penWidth);
|
|
||||||
|
|
||||||
|
|
||||||
QPen trackPen(QColor(255, 255, 255, 30), penWidth);
|
|
||||||
trackPen.setCapStyle(Qt::RoundCap);
|
|
||||||
p.setPen(trackPen);
|
|
||||||
p.drawArc(arcRect, 0, 360 * 16);
|
|
||||||
|
|
||||||
|
|
||||||
QPen sweepPen(QColor(96, 165, 250, 230), penWidth);
|
|
||||||
sweepPen.setCapStyle(Qt::RoundCap);
|
|
||||||
p.setPen(sweepPen);
|
|
||||||
const int startAngle = static_cast<int>(m_angle * 16);
|
|
||||||
p.drawArc(arcRect, startAngle, 90 * 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
qreal m_angle = 0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerMediaSwitcherPanel::PlayerMediaSwitcherPanel(QEmbyCore *core,
|
|
||||||
QWidget *parent)
|
|
||||||
: QFrame(parent) {
|
|
||||||
setObjectName("playerMediaSwitcherPanel");
|
|
||||||
setAttribute(Qt::WA_StyledBackground, true);
|
|
||||||
hide();
|
|
||||||
|
|
||||||
auto *layout = new QVBoxLayout(this);
|
|
||||||
layout->setContentsMargins(10, 6, 10, 6);
|
|
||||||
layout->setSpacing(3);
|
|
||||||
|
|
||||||
m_titleLabel = new QLabel(this);
|
|
||||||
m_titleLabel->setObjectName("playerMediaSwitcherTitle");
|
|
||||||
m_titleLabel->setWordWrap(true);
|
|
||||||
layout->addWidget(m_titleLabel);
|
|
||||||
|
|
||||||
m_statusLabel = new QLabel(this);
|
|
||||||
m_statusLabel->setObjectName("playerMediaSwitcherStatus");
|
|
||||||
m_statusLabel->setWordWrap(true);
|
|
||||||
layout->addWidget(m_statusLabel);
|
|
||||||
|
|
||||||
|
|
||||||
m_spinnerContainer = new QWidget(this);
|
|
||||||
m_spinnerContainer->setObjectName("playerMediaSwitcherLoading");
|
|
||||||
m_spinnerContainer->setFixedHeight(kSpinnerContainerHeight);
|
|
||||||
|
|
||||||
auto *spinnerLayout = new QHBoxLayout(m_spinnerContainer);
|
|
||||||
spinnerLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
spinnerLayout->addStretch();
|
|
||||||
|
|
||||||
auto *spinner = new SpinnerPainter(m_spinnerContainer);
|
|
||||||
m_spinnerWidget = spinner;
|
|
||||||
spinnerLayout->addWidget(spinner);
|
|
||||||
spinnerLayout->addStretch();
|
|
||||||
|
|
||||||
m_spinnerTimer = new QTimer(this);
|
|
||||||
m_spinnerTimer->setInterval(16);
|
|
||||||
connect(m_spinnerTimer, &QTimer::timeout, this, [this, spinner]() {
|
|
||||||
m_spinnerAngle -= 4.0;
|
|
||||||
if (m_spinnerAngle < -360.0)
|
|
||||||
m_spinnerAngle += 360.0;
|
|
||||||
spinner->setAngle(m_spinnerAngle);
|
|
||||||
});
|
|
||||||
|
|
||||||
layout->addWidget(m_spinnerContainer);
|
|
||||||
|
|
||||||
|
|
||||||
m_movieGallery = new HorizontalListViewGallery(core, this);
|
|
||||||
m_movieGallery->setObjectName("playerMediaSwitcherMovieGallery");
|
|
||||||
m_movieGallery->setCardStyle(MediaCardDelegate::Poster);
|
|
||||||
m_movieGallery->setTileSize(QSize(90, 154));
|
|
||||||
m_movieGallery->setTextPixelSizes(10, 9);
|
|
||||||
m_movieGallery->setContentPadding(5);
|
|
||||||
m_movieGallery->setHoverControls(MediaCardDelegate::HoverControlPlay);
|
|
||||||
m_movieGallery->setFixedHeight(kMovieGalleryHeight);
|
|
||||||
layout->addWidget(m_movieGallery);
|
|
||||||
|
|
||||||
m_seasonsLabel = new QLabel(tr("Seasons"), this);
|
|
||||||
m_seasonsLabel->setObjectName("playerMediaSwitcherSectionTitle");
|
|
||||||
layout->addWidget(m_seasonsLabel);
|
|
||||||
|
|
||||||
m_seasonGallery = new HorizontalListViewGallery(core, this);
|
|
||||||
m_seasonGallery->setObjectName("playerMediaSwitcherSeasonGallery");
|
|
||||||
m_seasonGallery->setCardStyle(MediaCardDelegate::Poster);
|
|
||||||
m_seasonGallery->setTileSize(QSize(74, 132));
|
|
||||||
m_seasonGallery->setTextPixelSizes(10, 9);
|
|
||||||
m_seasonGallery->setContentPadding(4);
|
|
||||||
m_seasonGallery->setHoverControls(
|
|
||||||
MediaCardDelegate::HoverControls());
|
|
||||||
m_seasonGallery->setFixedHeight(kSeasonGalleryHeight);
|
|
||||||
layout->addWidget(m_seasonGallery);
|
|
||||||
|
|
||||||
m_episodesLabel = new QLabel(tr("Episodes"), this);
|
|
||||||
m_episodesLabel->setObjectName("playerMediaSwitcherSectionTitle");
|
|
||||||
layout->addWidget(m_episodesLabel);
|
|
||||||
|
|
||||||
m_episodeGallery = new HorizontalListViewGallery(core, this);
|
|
||||||
m_episodeGallery->setObjectName("playerMediaSwitcherEpisodeGallery");
|
|
||||||
m_episodeGallery->setCardStyle(MediaCardDelegate::LibraryTile);
|
|
||||||
m_episodeGallery->setTileSize(QSize(154, 102));
|
|
||||||
m_episodeGallery->setTextPixelSizes(10, 9);
|
|
||||||
m_episodeGallery->setContentPadding(6);
|
|
||||||
m_episodeGallery->setHoverControls(MediaCardDelegate::HoverControlPlay);
|
|
||||||
m_episodeGallery->setFixedHeight(kEpisodeGalleryHeight);
|
|
||||||
layout->addWidget(m_episodeGallery);
|
|
||||||
|
|
||||||
connect(m_movieGallery, &HorizontalListViewGallery::itemClicked, this,
|
|
||||||
&PlayerMediaSwitcherPanel::onPlayableItemClicked);
|
|
||||||
connect(m_movieGallery, &HorizontalListViewGallery::playRequested, this,
|
|
||||||
&PlayerMediaSwitcherPanel::onPlayableItemClicked);
|
|
||||||
|
|
||||||
connect(m_seasonGallery, &HorizontalListViewGallery::itemClicked, this,
|
|
||||||
&PlayerMediaSwitcherPanel::onSeasonItemClicked);
|
|
||||||
connect(m_seasonGallery, &HorizontalListViewGallery::playRequested, this,
|
|
||||||
&PlayerMediaSwitcherPanel::onSeasonItemClicked);
|
|
||||||
|
|
||||||
connect(m_episodeGallery, &HorizontalListViewGallery::itemClicked, this,
|
|
||||||
&PlayerMediaSwitcherPanel::onPlayableItemClicked);
|
|
||||||
connect(m_episodeGallery, &HorizontalListViewGallery::playRequested, this,
|
|
||||||
&PlayerMediaSwitcherPanel::onPlayableItemClicked);
|
|
||||||
|
|
||||||
setStatusMessage(QString());
|
|
||||||
applySeriesMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
int PlayerMediaSwitcherPanel::preferredDrawerHeight() const {
|
|
||||||
auto *rootLayout = qobject_cast<QVBoxLayout *>(layout());
|
|
||||||
if (!rootLayout) {
|
|
||||||
return sizeHint().height();
|
|
||||||
}
|
|
||||||
|
|
||||||
const QMargins margins = rootLayout->contentsMargins();
|
|
||||||
int height = margins.top() + margins.bottom();
|
|
||||||
bool hasVisibleWidget = false;
|
|
||||||
|
|
||||||
const auto accumulateHeight = [&](const QWidget *widget) {
|
|
||||||
const int widgetHeight = visibleHeightForWidget(widget);
|
|
||||||
if (widgetHeight <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (hasVisibleWidget) {
|
|
||||||
height += rootLayout->spacing();
|
|
||||||
}
|
|
||||||
height += widgetHeight;
|
|
||||||
hasVisibleWidget = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
accumulateHeight(m_titleLabel);
|
|
||||||
accumulateHeight(m_statusLabel);
|
|
||||||
accumulateHeight(m_spinnerContainer);
|
|
||||||
accumulateHeight(m_movieGallery);
|
|
||||||
accumulateHeight(m_seasonsLabel);
|
|
||||||
accumulateHeight(m_seasonGallery);
|
|
||||||
accumulateHeight(m_episodesLabel);
|
|
||||||
accumulateHeight(m_episodeGallery);
|
|
||||||
|
|
||||||
return height;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerMediaSwitcherPanel::setLoadingState(const QString &title,
|
|
||||||
const QString &message,
|
|
||||||
bool seriesMode) {
|
|
||||||
m_titleLabel->setText(title);
|
|
||||||
m_isLoading = true;
|
|
||||||
m_loadingSeriesMode = seriesMode;
|
|
||||||
m_isSeriesMode = seriesMode;
|
|
||||||
m_currentMediaId.clear();
|
|
||||||
m_currentSeriesName.clear();
|
|
||||||
m_selectedSeasonId.clear();
|
|
||||||
m_movieItems.clear();
|
|
||||||
m_seasons.clear();
|
|
||||||
m_episodesBySeasonId.clear();
|
|
||||||
|
|
||||||
m_movieGallery->setItems(QList<MediaItem>{});
|
|
||||||
m_seasonGallery->setItems(QList<MediaItem>{});
|
|
||||||
m_episodeGallery->setItems(QList<MediaItem>{});
|
|
||||||
|
|
||||||
setStatusMessage(message);
|
|
||||||
applySeriesMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerMediaSwitcherPanel::setMovieItems(const QList<MediaItem> &items,
|
|
||||||
const QString ¤tMediaId) {
|
|
||||||
m_isLoading = false;
|
|
||||||
m_loadingSeriesMode = false;
|
|
||||||
m_isSeriesMode = false;
|
|
||||||
m_currentMediaId = currentMediaId;
|
|
||||||
m_currentSeriesName.clear();
|
|
||||||
m_selectedSeasonId.clear();
|
|
||||||
m_movieItems = items;
|
|
||||||
m_seasons.clear();
|
|
||||||
m_episodesBySeasonId.clear();
|
|
||||||
|
|
||||||
m_titleLabel->setText(tr("Continue Watching"));
|
|
||||||
populateMovieGallery();
|
|
||||||
applySeriesMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerMediaSwitcherPanel::setSeriesData(
|
|
||||||
const QString &seriesName, const QList<MediaItem> &seasons,
|
|
||||||
const QHash<QString, QList<MediaItem>> &episodesBySeasonId,
|
|
||||||
const QString ¤tMediaId) {
|
|
||||||
m_isLoading = false;
|
|
||||||
m_loadingSeriesMode = true;
|
|
||||||
m_isSeriesMode = true;
|
|
||||||
m_currentSeriesName = seriesName;
|
|
||||||
m_currentMediaId = currentMediaId;
|
|
||||||
m_movieItems.clear();
|
|
||||||
m_seasons = seasons;
|
|
||||||
m_episodesBySeasonId = episodesBySeasonId;
|
|
||||||
|
|
||||||
m_titleLabel->setText(seriesName.isEmpty() ? tr("Episodes") : seriesName);
|
|
||||||
|
|
||||||
m_selectedSeasonId.clear();
|
|
||||||
for (const MediaItem &season : m_seasons) {
|
|
||||||
const auto episodes = m_episodesBySeasonId.value(season.id);
|
|
||||||
for (const MediaItem &episode : episodes) {
|
|
||||||
if (episode.id == m_currentMediaId) {
|
|
||||||
m_selectedSeasonId = season.id;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!m_selectedSeasonId.isEmpty()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (m_selectedSeasonId.isEmpty() && !m_seasons.isEmpty()) {
|
|
||||||
m_selectedSeasonId = m_seasons.first().id;
|
|
||||||
}
|
|
||||||
|
|
||||||
populateSeasonGallery();
|
|
||||||
populateEpisodeGallery();
|
|
||||||
applySeriesMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerMediaSwitcherPanel::onSeasonItemClicked(const MediaItem &season) {
|
|
||||||
if (season.id.isEmpty() || season.id == m_selectedSeasonId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_selectedSeasonId = season.id;
|
|
||||||
populateSeasonGallery();
|
|
||||||
populateEpisodeGallery();
|
|
||||||
applySeriesMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerMediaSwitcherPanel::onPlayableItemClicked(const MediaItem &item) {
|
|
||||||
if (item.id.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Q_EMIT playRequested(item.id, formatResumeDisplayTitle(item),
|
|
||||||
item.userData.playbackPositionTicks);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerMediaSwitcherPanel::applySeriesMode() {
|
|
||||||
const bool hasStatus = !m_statusLabel->text().trimmed().isEmpty();
|
|
||||||
const bool showSeriesSections = !m_isLoading && m_isSeriesMode;
|
|
||||||
const bool hasSeasons = !m_seasons.isEmpty();
|
|
||||||
const bool hasEpisodeItems =
|
|
||||||
!m_episodesBySeasonId.value(currentSelectedSeasonId()).isEmpty();
|
|
||||||
|
|
||||||
m_statusLabel->setVisible(hasStatus);
|
|
||||||
|
|
||||||
|
|
||||||
m_spinnerContainer->setVisible(m_isLoading);
|
|
||||||
if (m_isLoading) {
|
|
||||||
m_spinnerAngle = 0.0;
|
|
||||||
m_spinnerTimer->start();
|
|
||||||
} else {
|
|
||||||
m_spinnerTimer->stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
m_movieGallery->setVisible(!m_isLoading && !m_isSeriesMode && !hasStatus);
|
|
||||||
m_seasonsLabel->setVisible(showSeriesSections && hasSeasons);
|
|
||||||
m_seasonGallery->setVisible(showSeriesSections && hasSeasons);
|
|
||||||
m_episodesLabel->setVisible(showSeriesSections && hasEpisodeItems);
|
|
||||||
m_episodeGallery->setVisible(showSeriesSections && hasEpisodeItems);
|
|
||||||
|
|
||||||
setFixedHeight(preferredDrawerHeight());
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerMediaSwitcherPanel::populateMovieGallery() {
|
|
||||||
setStatusMessage(QString());
|
|
||||||
m_movieGallery->setItems(m_movieItems);
|
|
||||||
m_movieGallery->setHighlightedItemId(m_currentMediaId);
|
|
||||||
const QString targetMovieId =
|
|
||||||
m_currentMediaId.isEmpty() && !m_movieItems.isEmpty()
|
|
||||||
? m_movieItems.first().id
|
|
||||||
: m_currentMediaId;
|
|
||||||
m_movieGallery->scrollToItemId(targetMovieId);
|
|
||||||
|
|
||||||
if (m_movieItems.isEmpty()) {
|
|
||||||
setStatusMessage(tr("No active media found."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerMediaSwitcherPanel::populateSeasonGallery() {
|
|
||||||
setStatusMessage(QString());
|
|
||||||
const QString selectedSeasonId = currentSelectedSeasonId();
|
|
||||||
m_seasonGallery->setItems(m_seasons);
|
|
||||||
m_seasonGallery->setHighlightedItemId(selectedSeasonId);
|
|
||||||
m_seasonGallery->scrollToItemId(selectedSeasonId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerMediaSwitcherPanel::populateEpisodeGallery() {
|
|
||||||
setStatusMessage(QString());
|
|
||||||
const QList<MediaItem> episodes =
|
|
||||||
m_episodesBySeasonId.value(currentSelectedSeasonId());
|
|
||||||
QString targetEpisodeId = m_currentMediaId;
|
|
||||||
bool hasCurrentEpisode = false;
|
|
||||||
for (const MediaItem &episode : episodes) {
|
|
||||||
if (episode.id == m_currentMediaId) {
|
|
||||||
hasCurrentEpisode = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!hasCurrentEpisode && !episodes.isEmpty()) {
|
|
||||||
targetEpisodeId = episodes.first().id;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_episodeGallery->setItems(episodes);
|
|
||||||
m_episodeGallery->setHighlightedItemId(m_currentMediaId);
|
|
||||||
m_episodeGallery->scrollToItemId(targetEpisodeId);
|
|
||||||
|
|
||||||
if (episodes.isEmpty()) {
|
|
||||||
setStatusMessage(tr("No episodes found."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerMediaSwitcherPanel::setStatusMessage(const QString &message) {
|
|
||||||
m_statusLabel->setText(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString PlayerMediaSwitcherPanel::formatEpisodeDisplayTitle(
|
|
||||||
const MediaItem &episode) const {
|
|
||||||
return MediaItemUtils::playbackTitle(episode, m_currentSeriesName);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString PlayerMediaSwitcherPanel::formatResumeDisplayTitle(
|
|
||||||
const MediaItem &item) const {
|
|
||||||
if (item.type == QStringLiteral("Episode")) {
|
|
||||||
return formatEpisodeDisplayTitle(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
return item.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString PlayerMediaSwitcherPanel::currentSelectedSeasonId() const {
|
|
||||||
if (!m_selectedSeasonId.isEmpty()) {
|
|
||||||
return m_selectedSeasonId;
|
|
||||||
}
|
|
||||||
return m_seasons.isEmpty() ? QString() : m_seasons.first().id;
|
|
||||||
}
|
|
||||||
|
|
||||||
int PlayerMediaSwitcherPanel::visibleHeightForWidget(const QWidget *widget) const
|
|
||||||
{
|
|
||||||
if (!widget || !widget->isVisible()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget->minimumHeight() == widget->maximumHeight() &&
|
|
||||||
widget->minimumHeight() > 0) {
|
|
||||||
return widget->minimumHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget->height() > 0) {
|
|
||||||
return widget->height();
|
|
||||||
}
|
|
||||||
|
|
||||||
return widget->sizeHint().height();
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
#ifndef PLAYERMEDIASWITCHERPANEL_H
|
|
||||||
#define PLAYERMEDIASWITCHERPANEL_H
|
|
||||||
|
|
||||||
#include <QFrame>
|
|
||||||
#include <QHash>
|
|
||||||
#include <QList>
|
|
||||||
|
|
||||||
#include <models/media/mediaitem.h>
|
|
||||||
|
|
||||||
class QLabel;
|
|
||||||
class QTimer;
|
|
||||||
class HorizontalListViewGallery;
|
|
||||||
class QEmbyCore;
|
|
||||||
class QWidget;
|
|
||||||
|
|
||||||
class PlayerMediaSwitcherPanel : public QFrame {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit PlayerMediaSwitcherPanel(QEmbyCore *core, QWidget *parent = nullptr);
|
|
||||||
~PlayerMediaSwitcherPanel() override = default;
|
|
||||||
|
|
||||||
int preferredDrawerHeight() const;
|
|
||||||
void setLoadingState(const QString &title, const QString &message,
|
|
||||||
bool seriesMode);
|
|
||||||
void setMovieItems(const QList<MediaItem> &items, const QString ¤tMediaId);
|
|
||||||
void setSeriesData(const QString &seriesName,
|
|
||||||
const QList<MediaItem> &seasons,
|
|
||||||
const QHash<QString, QList<MediaItem>> &episodesBySeasonId,
|
|
||||||
const QString ¤tMediaId);
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void playRequested(QString mediaId, QString displayTitle,
|
|
||||||
long long startPositionTicks);
|
|
||||||
|
|
||||||
private Q_SLOTS:
|
|
||||||
void onSeasonItemClicked(const MediaItem &season);
|
|
||||||
void onPlayableItemClicked(const MediaItem &item);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void applySeriesMode();
|
|
||||||
void populateMovieGallery();
|
|
||||||
void populateSeasonGallery();
|
|
||||||
void populateEpisodeGallery();
|
|
||||||
void setStatusMessage(const QString &message);
|
|
||||||
QString formatEpisodeDisplayTitle(const MediaItem &episode) const;
|
|
||||||
QString formatResumeDisplayTitle(const MediaItem &item) const;
|
|
||||||
QString currentSelectedSeasonId() const;
|
|
||||||
int visibleHeightForWidget(const QWidget *widget) const;
|
|
||||||
|
|
||||||
QLabel *m_titleLabel = nullptr;
|
|
||||||
QLabel *m_statusLabel = nullptr;
|
|
||||||
QLabel *m_seasonsLabel = nullptr;
|
|
||||||
QLabel *m_episodesLabel = nullptr;
|
|
||||||
QWidget *m_spinnerContainer = nullptr;
|
|
||||||
QWidget *m_spinnerWidget = nullptr;
|
|
||||||
QTimer *m_spinnerTimer = nullptr;
|
|
||||||
qreal m_spinnerAngle = 0.0;
|
|
||||||
HorizontalListViewGallery *m_movieGallery = nullptr;
|
|
||||||
HorizontalListViewGallery *m_seasonGallery = nullptr;
|
|
||||||
HorizontalListViewGallery *m_episodeGallery = nullptr;
|
|
||||||
|
|
||||||
QList<MediaItem> m_movieItems;
|
|
||||||
QList<MediaItem> m_seasons;
|
|
||||||
QHash<QString, QList<MediaItem>> m_episodesBySeasonId;
|
|
||||||
QString m_currentMediaId;
|
|
||||||
QString m_currentSeriesName;
|
|
||||||
QString m_selectedSeasonId;
|
|
||||||
bool m_isLoading = false;
|
|
||||||
bool m_loadingSeriesMode = false;
|
|
||||||
bool m_isSeriesMode = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,354 +0,0 @@
|
|||||||
#include "playerosdlayer.h"
|
|
||||||
#include "../tokens/AnimationTokens.h"
|
|
||||||
#include "../utils/motionprefs.h"
|
|
||||||
#include <QAbstractAnimation>
|
|
||||||
#include <QGraphicsOpacityEffect>
|
|
||||||
#include <QIcon>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QProgressBar>
|
|
||||||
#include <QPropertyAnimation>
|
|
||||||
#include <QTimer>
|
|
||||||
#include <QWidget>
|
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
constexpr int kSeekLineHeight = 4;
|
|
||||||
constexpr int kSeekLabelMinWidth = 80;
|
|
||||||
constexpr int kSeekLabelMaxWidth = 148;
|
|
||||||
constexpr int kSeekLabelHeight = 28;
|
|
||||||
constexpr int kSeekLabelBottomOffset = 46;
|
|
||||||
constexpr int kSeekHorizontalMargin = 10;
|
|
||||||
constexpr int kSeekMarkerSize = 7;
|
|
||||||
constexpr int kVolumePanelWidth = 292;
|
|
||||||
constexpr int kVolumePanelHeight = 46;
|
|
||||||
constexpr int kVolumePanelMargin = 16;
|
|
||||||
constexpr int kVolumePanelTopMin = 72;
|
|
||||||
constexpr int kVolumeIconSize = 20;
|
|
||||||
constexpr int kVolumeTextWidth = 46;
|
|
||||||
constexpr int kVolumeContentPadding = 18;
|
|
||||||
constexpr int kVolumeContentGap = 14;
|
|
||||||
constexpr int kVolumeBarHeight = 5;
|
|
||||||
|
|
||||||
void stopFadeAnimationSafely(QPropertyAnimation *animation)
|
|
||||||
{
|
|
||||||
if (!animation || !animation->targetObject() ||
|
|
||||||
animation->state() == QAbstractAnimation::Stopped)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
animation->stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerOsdLayer::PlayerOsdLayer(QWidget *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
{
|
|
||||||
|
|
||||||
m_container = new QWidget(parent);
|
|
||||||
m_container->setObjectName("osdContainer");
|
|
||||||
m_container->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
||||||
m_container->hide();
|
|
||||||
|
|
||||||
|
|
||||||
m_opacity = new QGraphicsOpacityEffect(m_container);
|
|
||||||
m_opacity->setOpacity(0.0);
|
|
||||||
m_container->setGraphicsEffect(m_opacity);
|
|
||||||
|
|
||||||
|
|
||||||
m_fadeAnim = new QPropertyAnimation(m_opacity, "opacity", this);
|
|
||||||
m_fadeAnim->setDuration(AnimationTokens::DurationMs::kNormal);
|
|
||||||
m_fadeAnim->setEasingCurve(AnimationTokens::Curve::fade());
|
|
||||||
|
|
||||||
|
|
||||||
m_hideTimer = new QTimer(this);
|
|
||||||
m_hideTimer->setSingleShot(true);
|
|
||||||
connect(m_hideTimer, &QTimer::timeout, this, &PlayerOsdLayer::hide);
|
|
||||||
|
|
||||||
|
|
||||||
m_seekLine = new QProgressBar(m_container);
|
|
||||||
m_seekLine->setObjectName("osdSeekLine");
|
|
||||||
m_seekLine->setTextVisible(false);
|
|
||||||
m_seekLine->setFixedHeight(kSeekLineHeight);
|
|
||||||
|
|
||||||
|
|
||||||
m_seekTimeLabel = new QLabel(m_container);
|
|
||||||
m_seekTimeLabel->setObjectName("osdSeekTimeLabel");
|
|
||||||
m_seekTimeLabel->setAlignment(Qt::AlignCenter);
|
|
||||||
m_seekTimeLabel->setMinimumWidth(kSeekLabelMinWidth);
|
|
||||||
|
|
||||||
|
|
||||||
m_seekStem = new QWidget(m_container);
|
|
||||||
m_seekStem->setObjectName("osdSeekStem");
|
|
||||||
|
|
||||||
m_seekMarker = new QWidget(m_container);
|
|
||||||
m_seekMarker->setObjectName("osdSeekMarker");
|
|
||||||
|
|
||||||
|
|
||||||
m_volumePanel = new QWidget(m_container);
|
|
||||||
m_volumePanel->setObjectName("osdVolumePanel");
|
|
||||||
m_volumePanel->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
||||||
m_volumePanel->hide();
|
|
||||||
|
|
||||||
m_volumeIconLabel = new QLabel(m_volumePanel);
|
|
||||||
m_volumeIconLabel->setObjectName("osdVolumeIconLabel");
|
|
||||||
m_volumeIconLabel->setAlignment(Qt::AlignCenter);
|
|
||||||
|
|
||||||
m_volumeBar = new QProgressBar(m_volumePanel);
|
|
||||||
m_volumeBar->setObjectName("osdVolumeBar");
|
|
||||||
m_volumeBar->setTextVisible(false);
|
|
||||||
m_volumeBar->setRange(0, 100);
|
|
||||||
m_volumeBar->setFixedHeight(kVolumeBarHeight);
|
|
||||||
|
|
||||||
m_volumeLabel = new QLabel(m_volumePanel);
|
|
||||||
m_volumeLabel->setObjectName("osdVolumeLabel");
|
|
||||||
m_volumeLabel->setAlignment(Qt::AlignCenter);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerOsdLayer::showSeek(double position, double duration, const QString &timeText)
|
|
||||||
{
|
|
||||||
fadeIn();
|
|
||||||
|
|
||||||
m_seekLine->show();
|
|
||||||
m_seekTimeLabel->show();
|
|
||||||
m_seekStem->show();
|
|
||||||
m_seekMarker->show();
|
|
||||||
m_volumePanel->hide();
|
|
||||||
|
|
||||||
m_seekDuration = std::isfinite(duration) && duration > 0.0 ? duration : 0.0;
|
|
||||||
m_seekPosition = std::isfinite(position) ? position : 0.0;
|
|
||||||
if (m_seekDuration > 0.0)
|
|
||||||
{
|
|
||||||
m_seekPosition = qBound(0.0, m_seekPosition, m_seekDuration);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
m_seekPosition = 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int maximum = qMax(1, static_cast<int>(std::ceil(m_seekDuration)));
|
|
||||||
m_seekLine->setRange(0, maximum);
|
|
||||||
m_seekLine->setValue(qBound(0, static_cast<int>(std::round(m_seekPosition)), maximum));
|
|
||||||
m_seekTimeLabel->setText(timeText);
|
|
||||||
updateSeekLayout();
|
|
||||||
|
|
||||||
m_hideTimer->start(1500);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerOsdLayer::showVolume(int volumePercent, const QString &text, bool muted)
|
|
||||||
{
|
|
||||||
fadeIn();
|
|
||||||
|
|
||||||
m_seekLine->hide();
|
|
||||||
m_seekTimeLabel->hide();
|
|
||||||
m_seekStem->hide();
|
|
||||||
m_seekMarker->hide();
|
|
||||||
m_volumePanel->show();
|
|
||||||
|
|
||||||
m_volumePercent = qBound(0, volumePercent, 100);
|
|
||||||
const bool effectiveMuted = muted || m_volumePercent <= 0;
|
|
||||||
const QString iconPath =
|
|
||||||
effectiveMuted ? QStringLiteral(":/svg/player/volume-mute.svg") : QStringLiteral(":/svg/player/volume.svg");
|
|
||||||
m_volumeIconLabel->setPixmap(QIcon(iconPath).pixmap(kVolumeIconSize, kVolumeIconSize));
|
|
||||||
m_volumeBar->setValue(m_volumePercent);
|
|
||||||
m_volumeLabel->setText(text);
|
|
||||||
updateVolumeLayout();
|
|
||||||
|
|
||||||
m_hideTimer->start(1500);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerOsdLayer::hide()
|
|
||||||
{
|
|
||||||
if (m_opacity->opacity() <= 0.0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stopFadeAnimationSafely(m_fadeAnim);
|
|
||||||
m_fadeAnim->setStartValue(m_opacity->opacity());
|
|
||||||
m_fadeAnim->setEndValue(0.0);
|
|
||||||
if (canStartAnimation())
|
|
||||||
{
|
|
||||||
m_fadeAnim->start();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
m_opacity->setOpacity(0.0);
|
|
||||||
if (m_container)
|
|
||||||
{
|
|
||||||
m_container->hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerOsdLayer::forceHide()
|
|
||||||
{
|
|
||||||
if (m_hideTimer)
|
|
||||||
{
|
|
||||||
m_hideTimer->stop();
|
|
||||||
}
|
|
||||||
stopFadeAnimationSafely(m_fadeAnim);
|
|
||||||
if (m_opacity)
|
|
||||||
{
|
|
||||||
m_opacity->setOpacity(0.0);
|
|
||||||
}
|
|
||||||
if (m_container)
|
|
||||||
{
|
|
||||||
m_container->hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerOsdLayer::updateGeometry(int parentWidth, int parentHeight)
|
|
||||||
{
|
|
||||||
m_parentWidth = parentWidth;
|
|
||||||
m_parentHeight = parentHeight;
|
|
||||||
|
|
||||||
m_container->setGeometry(0, 0, parentWidth, parentHeight);
|
|
||||||
m_seekLine->setGeometry(0, parentHeight - kSeekLineHeight, parentWidth, kSeekLineHeight);
|
|
||||||
updateSeekLayout();
|
|
||||||
updateVolumeLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerOsdLayer::stopAnimations()
|
|
||||||
{
|
|
||||||
if (m_hideTimer)
|
|
||||||
{
|
|
||||||
m_hideTimer->stop();
|
|
||||||
}
|
|
||||||
if (m_fadeAnim)
|
|
||||||
{
|
|
||||||
stopFadeAnimationSafely(m_fadeAnim);
|
|
||||||
}
|
|
||||||
if (m_opacity)
|
|
||||||
{
|
|
||||||
m_opacity->setOpacity(0.0);
|
|
||||||
}
|
|
||||||
if (m_container)
|
|
||||||
{
|
|
||||||
m_container->hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PlayerOsdLayer::isVisible() const
|
|
||||||
{
|
|
||||||
return m_container && m_container->isVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PlayerOsdLayer::isSeekLineVisible() const
|
|
||||||
{
|
|
||||||
return isVisible() && m_seekLine && !m_seekLine->isHidden();
|
|
||||||
}
|
|
||||||
|
|
||||||
QWidget *PlayerOsdLayer::container() const
|
|
||||||
{
|
|
||||||
return m_container;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerOsdLayer::updateSeekPosition(int position, const QString &timeText)
|
|
||||||
{
|
|
||||||
m_seekPosition = position;
|
|
||||||
if (m_seekDuration > 0.0)
|
|
||||||
{
|
|
||||||
m_seekPosition = qBound(0.0, m_seekPosition, m_seekDuration);
|
|
||||||
}
|
|
||||||
|
|
||||||
const int maximum = qMax(1, m_seekLine->maximum());
|
|
||||||
m_seekLine->setValue(qBound(0, static_cast<int>(std::round(m_seekPosition)), maximum));
|
|
||||||
m_seekTimeLabel->setText(timeText);
|
|
||||||
updateSeekLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerOsdLayer::fadeIn()
|
|
||||||
{
|
|
||||||
if (!m_container->isVisible() || m_opacity->opacity() == 0.0)
|
|
||||||
{
|
|
||||||
m_container->show();
|
|
||||||
stopFadeAnimationSafely(m_fadeAnim);
|
|
||||||
m_fadeAnim->setStartValue(m_opacity->opacity());
|
|
||||||
m_fadeAnim->setEndValue(1.0);
|
|
||||||
if (canStartAnimation())
|
|
||||||
{
|
|
||||||
m_fadeAnim->start();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
m_opacity->setOpacity(1.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PlayerOsdLayer::canStartAnimation() const
|
|
||||||
{
|
|
||||||
return m_fadeAnim && m_fadeAnim->targetObject() && !MotionPrefs::reduceMotion();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerOsdLayer::updateSeekLayout()
|
|
||||||
{
|
|
||||||
if (!m_seekTimeLabel || !m_seekStem || !m_seekMarker || m_parentWidth <= 0 || m_parentHeight <= 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const double progressRatio = m_seekDuration > 0.0 ? qBound(0.0, m_seekPosition / m_seekDuration, 1.0) : 0.0;
|
|
||||||
const int progressX = qBound(0, static_cast<int>(std::round(progressRatio * (m_parentWidth - 1))),
|
|
||||||
qMax(0, m_parentWidth - 1));
|
|
||||||
|
|
||||||
const int preferredWidth = m_seekTimeLabel->sizeHint().width();
|
|
||||||
const int labelWidth = qBound(kSeekLabelMinWidth, preferredWidth, kSeekLabelMaxWidth);
|
|
||||||
const int labelY = qMax(12, m_parentHeight - kSeekLabelBottomOffset);
|
|
||||||
|
|
||||||
int labelX = 0;
|
|
||||||
if (m_parentWidth <= labelWidth + kSeekHorizontalMargin * 2)
|
|
||||||
{
|
|
||||||
labelX = qMax(0, (m_parentWidth - labelWidth) / 2);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
labelX = qBound(kSeekHorizontalMargin, progressX - labelWidth / 2,
|
|
||||||
m_parentWidth - labelWidth - kSeekHorizontalMargin);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_seekTimeLabel->setGeometry(labelX, labelY, labelWidth, kSeekLabelHeight);
|
|
||||||
|
|
||||||
const int markerX = qBound(0, progressX - kSeekMarkerSize / 2, qMax(0, m_parentWidth - kSeekMarkerSize));
|
|
||||||
const int markerY = qMax(labelY + kSeekLabelHeight + 3, m_parentHeight - kSeekLineHeight - kSeekMarkerSize - 1);
|
|
||||||
m_seekMarker->setGeometry(markerX, markerY, kSeekMarkerSize, kSeekMarkerSize);
|
|
||||||
|
|
||||||
const int stemTop = labelY + kSeekLabelHeight + 2;
|
|
||||||
const int stemBottom = qMax(stemTop, markerY - 1);
|
|
||||||
const int stemHeight = qMax(2, stemBottom - stemTop);
|
|
||||||
const int stemX = qBound(0, progressX, qMax(0, m_parentWidth - 1));
|
|
||||||
m_seekStem->setGeometry(stemX, stemTop, 1, stemHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerOsdLayer::updateVolumeLayout()
|
|
||||||
{
|
|
||||||
if (!m_volumePanel || !m_volumeIconLabel || !m_volumeBar || !m_volumeLabel || m_parentWidth <= 0 ||
|
|
||||||
m_parentHeight <= 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int panelWidth = qMin(kVolumePanelWidth, qMax(180, m_parentWidth - kVolumePanelMargin * 2));
|
|
||||||
const int panelX = qMax(0, (m_parentWidth - panelWidth) / 2);
|
|
||||||
const int preferredY = static_cast<int>(std::round(m_parentHeight * 0.22));
|
|
||||||
const int maxPanelY = qMax(0, m_parentHeight - kVolumePanelHeight - kVolumePanelMargin);
|
|
||||||
const int panelY = qMin(qMax(kVolumePanelTopMin, preferredY), maxPanelY);
|
|
||||||
m_volumePanel->setGeometry(panelX, panelY, panelWidth, kVolumePanelHeight);
|
|
||||||
|
|
||||||
const int availableWidth = panelWidth - kVolumeContentPadding * 2;
|
|
||||||
const int barWidth =
|
|
||||||
qMax(86, availableWidth - kVolumeIconSize - kVolumeTextWidth - kVolumeContentGap * 2);
|
|
||||||
const int contentWidth = kVolumeIconSize + kVolumeContentGap + barWidth + kVolumeContentGap + kVolumeTextWidth;
|
|
||||||
const int contentX = qMax(kVolumeContentPadding, (panelWidth - contentWidth) / 2);
|
|
||||||
|
|
||||||
const int iconY = (kVolumePanelHeight - kVolumeIconSize) / 2;
|
|
||||||
m_volumeIconLabel->setGeometry(contentX, iconY, kVolumeIconSize, kVolumeIconSize);
|
|
||||||
|
|
||||||
const int barX = contentX + kVolumeIconSize + kVolumeContentGap;
|
|
||||||
const int barY = (kVolumePanelHeight - kVolumeBarHeight) / 2;
|
|
||||||
m_volumeBar->setGeometry(barX, barY, barWidth, kVolumeBarHeight);
|
|
||||||
|
|
||||||
const int textX = barX + barWidth + kVolumeContentGap;
|
|
||||||
m_volumeLabel->setGeometry(textX, 0, kVolumeTextWidth, kVolumePanelHeight);
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
#ifndef PLAYEROSDLAYER_H
|
|
||||||
#define PLAYEROSDLAYER_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
|
|
||||||
class QWidget;
|
|
||||||
class QLabel;
|
|
||||||
class QProgressBar;
|
|
||||||
class QGraphicsOpacityEffect;
|
|
||||||
class QPropertyAnimation;
|
|
||||||
class QTimer;
|
|
||||||
|
|
||||||
|
|
||||||
class PlayerOsdLayer : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit PlayerOsdLayer(QWidget *parent);
|
|
||||||
|
|
||||||
|
|
||||||
void showSeek(double position, double duration, const QString &timeText);
|
|
||||||
|
|
||||||
|
|
||||||
void showVolume(int volumePercent, const QString &text, bool muted);
|
|
||||||
|
|
||||||
|
|
||||||
void hide();
|
|
||||||
|
|
||||||
|
|
||||||
void forceHide();
|
|
||||||
|
|
||||||
|
|
||||||
void updateGeometry(int parentWidth, int parentHeight);
|
|
||||||
|
|
||||||
|
|
||||||
void stopAnimations();
|
|
||||||
|
|
||||||
|
|
||||||
bool isVisible() const;
|
|
||||||
|
|
||||||
|
|
||||||
bool isSeekLineVisible() const;
|
|
||||||
|
|
||||||
|
|
||||||
QWidget *container() const;
|
|
||||||
|
|
||||||
|
|
||||||
void updateSeekPosition(int position, const QString &timeText);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void fadeIn();
|
|
||||||
bool canStartAnimation() const;
|
|
||||||
void updateSeekLayout();
|
|
||||||
void updateVolumeLayout();
|
|
||||||
|
|
||||||
QWidget *m_container = nullptr;
|
|
||||||
QProgressBar *m_seekLine = nullptr;
|
|
||||||
QLabel *m_seekTimeLabel = nullptr;
|
|
||||||
QWidget *m_seekStem = nullptr;
|
|
||||||
QWidget *m_seekMarker = nullptr;
|
|
||||||
QWidget *m_volumePanel = nullptr;
|
|
||||||
QLabel *m_volumeIconLabel = nullptr;
|
|
||||||
QProgressBar *m_volumeBar = nullptr;
|
|
||||||
QLabel *m_volumeLabel = nullptr;
|
|
||||||
QGraphicsOpacityEffect *m_opacity = nullptr;
|
|
||||||
QPropertyAnimation *m_fadeAnim = nullptr;
|
|
||||||
QTimer *m_hideTimer = nullptr;
|
|
||||||
int m_parentWidth = 0;
|
|
||||||
int m_parentHeight = 0;
|
|
||||||
double m_seekPosition = 0.0;
|
|
||||||
double m_seekDuration = 0.0;
|
|
||||||
int m_volumePercent = 100;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
#include "playerstatisticsoverlay.h"
|
|
||||||
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QPaintEvent>
|
|
||||||
#include <QTextLayout>
|
|
||||||
#include <QTextOption>
|
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
void configureWrappedLayout(QTextLayout *layout,
|
|
||||||
const QString &text,
|
|
||||||
const QFont &font,
|
|
||||||
qreal width)
|
|
||||||
{
|
|
||||||
if (!layout)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
layout->clearLayout();
|
|
||||||
layout->setText(text);
|
|
||||||
layout->setFont(font);
|
|
||||||
QTextOption option(Qt::AlignLeft | Qt::AlignTop);
|
|
||||||
option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
|
|
||||||
layout->setTextOption(option);
|
|
||||||
|
|
||||||
layout->beginLayout();
|
|
||||||
qreal y = 0.0;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
QTextLine line = layout->createLine();
|
|
||||||
if (!line.isValid())
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
line.setLineWidth(width);
|
|
||||||
line.setPosition(QPointF(0.0, y));
|
|
||||||
y += line.height();
|
|
||||||
}
|
|
||||||
layout->endLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
qreal layoutHeight(const QTextLayout &layout)
|
|
||||||
{
|
|
||||||
qreal height = 0.0;
|
|
||||||
for (int i = 0; i < layout.lineCount(); ++i)
|
|
||||||
{
|
|
||||||
const QTextLine line = layout.lineAt(i);
|
|
||||||
height = qMax(height, line.y() + line.height());
|
|
||||||
}
|
|
||||||
return height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerStatisticsOverlay::PlayerStatisticsOverlay(QWidget *parent)
|
|
||||||
: QWidget(parent)
|
|
||||||
{
|
|
||||||
setAttribute(Qt::WA_TranslucentBackground);
|
|
||||||
setAttribute(Qt::WA_NoSystemBackground);
|
|
||||||
setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
||||||
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
||||||
|
|
||||||
QFont overlayFont(QStringLiteral("Microsoft YaHei UI"));
|
|
||||||
overlayFont.setPointSize(10);
|
|
||||||
overlayFont.setWeight(QFont::DemiBold);
|
|
||||||
setFont(overlayFont);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerStatisticsOverlay::setLines(const QStringList &lines)
|
|
||||||
{
|
|
||||||
if (m_lines == lines)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_lines = lines;
|
|
||||||
updateGeometry();
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerStatisticsOverlay::setTextColor(const QColor &color)
|
|
||||||
{
|
|
||||||
if (m_textColor == color)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_textColor = color;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerStatisticsOverlay::setShadowColor(const QColor &color)
|
|
||||||
{
|
|
||||||
if (m_shadowColor == color)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_shadowColor = color;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize PlayerStatisticsOverlay::sizeHint() const
|
|
||||||
{
|
|
||||||
const int targetWidth = width() > 0 ? width() : 520;
|
|
||||||
const qreal contentWidth = qMax<qreal>(160.0, targetWidth - (m_horizontalPadding * 2));
|
|
||||||
|
|
||||||
qreal totalHeight = m_verticalPadding * 2;
|
|
||||||
bool hadVisibleLine = false;
|
|
||||||
|
|
||||||
for (const QString &line : m_lines)
|
|
||||||
{
|
|
||||||
if (line.trimmed().isEmpty())
|
|
||||||
{
|
|
||||||
if (hadVisibleLine)
|
|
||||||
{
|
|
||||||
totalHeight += m_sectionSpacing;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
totalHeight += textHeightForWidth(line, contentWidth);
|
|
||||||
totalHeight += m_rowSpacing;
|
|
||||||
hadVisibleLine = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hadVisibleLine)
|
|
||||||
{
|
|
||||||
totalHeight -= m_rowSpacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
return QSize(targetWidth, qMax(0, static_cast<int>(std::ceil(totalHeight))));
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize PlayerStatisticsOverlay::minimumSizeHint() const
|
|
||||||
{
|
|
||||||
return sizeHint();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerStatisticsOverlay::paintEvent(QPaintEvent *event)
|
|
||||||
{
|
|
||||||
Q_UNUSED(event);
|
|
||||||
|
|
||||||
QPainter painter(this);
|
|
||||||
painter.setRenderHint(QPainter::TextAntialiasing, true);
|
|
||||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
|
||||||
|
|
||||||
const qreal contentWidth = qMax<qreal>(160.0, width() - (m_horizontalPadding * 2));
|
|
||||||
qreal y = m_verticalPadding;
|
|
||||||
bool hadVisibleLine = false;
|
|
||||||
|
|
||||||
for (const QString &line : m_lines)
|
|
||||||
{
|
|
||||||
if (line.trimmed().isEmpty())
|
|
||||||
{
|
|
||||||
if (hadVisibleLine)
|
|
||||||
{
|
|
||||||
y += m_sectionSpacing;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const qreal blockHeight = textHeightForWidth(line, contentWidth);
|
|
||||||
const QPointF origin(m_horizontalPadding, y);
|
|
||||||
|
|
||||||
static const QList<QPointF> shadowOffsets = {
|
|
||||||
QPointF(-1.0, 0.0), QPointF(1.0, 0.0), QPointF(0.0, -1.0),
|
|
||||||
QPointF(0.0, 1.0), QPointF(-1.0, -1.0), QPointF(1.0, -1.0),
|
|
||||||
QPointF(-1.0, 1.0), QPointF(1.0, 1.0)};
|
|
||||||
|
|
||||||
for (const QPointF &offset : shadowOffsets)
|
|
||||||
{
|
|
||||||
drawTextBlock(&painter, line, origin + offset, contentWidth, m_shadowColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawTextBlock(&painter, line, origin, contentWidth, m_textColor);
|
|
||||||
|
|
||||||
y += blockHeight + m_rowSpacing;
|
|
||||||
hadVisibleLine = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
qreal PlayerStatisticsOverlay::textHeightForWidth(const QString &text, qreal width) const
|
|
||||||
{
|
|
||||||
QTextLayout layout;
|
|
||||||
configureWrappedLayout(&layout, text, font(), width);
|
|
||||||
return layoutHeight(layout);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerStatisticsOverlay::drawTextBlock(QPainter *painter,
|
|
||||||
const QString &text,
|
|
||||||
const QPointF &topLeft,
|
|
||||||
qreal width,
|
|
||||||
const QColor &color) const
|
|
||||||
{
|
|
||||||
QTextLayout layout;
|
|
||||||
configureWrappedLayout(&layout, text, font(), width);
|
|
||||||
|
|
||||||
painter->save();
|
|
||||||
painter->setPen(color);
|
|
||||||
layout.draw(painter, topLeft);
|
|
||||||
painter->restore();
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#ifndef PLAYERSTATISTICSOVERLAY_H
|
|
||||||
#define PLAYERSTATISTICSOVERLAY_H
|
|
||||||
|
|
||||||
#include <QColor>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QWidget>
|
|
||||||
|
|
||||||
class QPainter;
|
|
||||||
class QPaintEvent;
|
|
||||||
class QPointF;
|
|
||||||
|
|
||||||
class PlayerStatisticsOverlay : public QWidget
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit PlayerStatisticsOverlay(QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
void setLines(const QStringList &lines);
|
|
||||||
QStringList lines() const { return m_lines; }
|
|
||||||
|
|
||||||
QColor textColor() const { return m_textColor; }
|
|
||||||
void setTextColor(const QColor &color);
|
|
||||||
|
|
||||||
QColor shadowColor() const { return m_shadowColor; }
|
|
||||||
void setShadowColor(const QColor &color);
|
|
||||||
|
|
||||||
QSize sizeHint() const override;
|
|
||||||
QSize minimumSizeHint() const override;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void paintEvent(QPaintEvent *event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
qreal textHeightForWidth(const QString &text, qreal width) const;
|
|
||||||
void drawTextBlock(QPainter *painter,
|
|
||||||
const QString &text,
|
|
||||||
const QPointF &topLeft,
|
|
||||||
qreal width,
|
|
||||||
const QColor &color) const;
|
|
||||||
|
|
||||||
QStringList m_lines;
|
|
||||||
QColor m_textColor = Qt::white;
|
|
||||||
QColor m_shadowColor = QColor(0, 0, 0, 230);
|
|
||||||
int m_horizontalPadding = 4;
|
|
||||||
int m_verticalPadding = 4;
|
|
||||||
int m_rowSpacing = 2;
|
|
||||||
int m_sectionSpacing = 8;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
#include "playerwindow.h"
|
|
||||||
#include "../views/media/playerview.h"
|
|
||||||
#include <QPoint>
|
|
||||||
#include <QRect>
|
|
||||||
#include <QSize>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
#include <QWKWidgets/widgetwindowagent.h>
|
|
||||||
|
|
||||||
PlayerWindow::PlayerWindow(QEmbyCore* core, QWidget *parent)
|
|
||||||
: QWidget(parent)
|
|
||||||
{
|
|
||||||
#if (defined(Q_OS_MACOS) || defined(Q_OS_MAC)) && \
|
|
||||||
(QT_VERSION >= QT_VERSION_CHECK(6, 9, 0))
|
|
||||||
setWindowFlag(Qt::ExpandedClientAreaHint, true);
|
|
||||||
setWindowFlag(Qt::NoTitleBarBackgroundHint, true);
|
|
||||||
setAttribute(Qt::WA_ContentsMarginsRespectsSafeArea, false);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
m_windowAgent = new QWK::WidgetWindowAgent(this);
|
|
||||||
m_windowAgent->setup(this);
|
|
||||||
#if defined(Q_OS_MACOS) || defined(Q_OS_MAC)
|
|
||||||
m_windowAgent->setWindowAttribute("no-system-buttons", false);
|
|
||||||
#endif
|
|
||||||
#if defined(Q_OS_MAC)
|
|
||||||
m_windowAgent->setSystemButtonAreaCallback([](const QSize &size) {
|
|
||||||
return QRect(QPoint(0, 0), QSize(80, size.height()));
|
|
||||||
});
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
m_playerView = new PlayerView(core, this);
|
|
||||||
|
|
||||||
auto *layout = new QVBoxLayout(this);
|
|
||||||
layout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
layout->setSpacing(0);
|
|
||||||
layout->addWidget(m_playerView);
|
|
||||||
|
|
||||||
|
|
||||||
QWidget* topHUD = m_playerView->findChild<QWidget*>("playerTopHUD");
|
|
||||||
if (topHUD) {
|
|
||||||
m_windowAgent->setTitleBar(topHUD);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#if !defined(Q_OS_MACOS) && !defined(Q_OS_MAC)
|
|
||||||
if (auto* minBtn = m_playerView->findChild<QWidget*>("hud-min-btn"))
|
|
||||||
m_windowAgent->setSystemButton(QWK::WindowAgentBase::Minimize, minBtn);
|
|
||||||
if (auto* maxBtn = m_playerView->findChild<QWidget*>("hud-max-btn"))
|
|
||||||
m_windowAgent->setSystemButton(QWK::WindowAgentBase::Maximize, maxBtn);
|
|
||||||
if (auto* closeBtn = m_playerView->findChild<QWidget*>("hud-close-btn"))
|
|
||||||
m_windowAgent->setSystemButton(QWK::WindowAgentBase::Close, closeBtn);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
if (auto* backBtn = m_playerView->findChild<QWidget*>("hud-back-btn"))
|
|
||||||
m_windowAgent->setHitTestVisible(backBtn, true);
|
|
||||||
|
|
||||||
connect(m_playerView, &PlayerView::playerChromeVisibilityChanged, this,
|
|
||||||
&PlayerWindow::setMacSystemButtonsVisible);
|
|
||||||
connect(m_playerView, &PlayerView::playbackTitleChanged, this,
|
|
||||||
[this](const QString &title) { setWindowTitle(title); });
|
|
||||||
|
|
||||||
|
|
||||||
connect(m_playerView, &BaseView::navigateBack, this, [this]() {
|
|
||||||
close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerWindow::playMedia(const QString &mediaId, const QString &title,
|
|
||||||
const QString &streamUrl, long long startPositionTicks,
|
|
||||||
const QVariant& sourceInfoVar)
|
|
||||||
{
|
|
||||||
setWindowTitle(title);
|
|
||||||
m_playerView->playMedia(mediaId, title, streamUrl, startPositionTicks, sourceInfoVar);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerWindow::setMacSystemButtonsVisible(bool visible)
|
|
||||||
{
|
|
||||||
#if defined(Q_OS_MACOS) || defined(Q_OS_MAC)
|
|
||||||
if (m_windowAgent) {
|
|
||||||
m_windowAgent->setWindowAttribute("no-system-buttons", !visible);
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
Q_UNUSED(visible);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
#ifndef PLAYERWINDOW_H
|
|
||||||
#define PLAYERWINDOW_H
|
|
||||||
|
|
||||||
#include <QWidget>
|
|
||||||
#include <QPointer>
|
|
||||||
|
|
||||||
class QEmbyCore;
|
|
||||||
class PlayerView;
|
|
||||||
namespace QWK {
|
|
||||||
class WidgetWindowAgent;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PlayerWindow : public QWidget
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit PlayerWindow(QEmbyCore* core, QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
void playMedia(const QString &mediaId, const QString &title,
|
|
||||||
const QString &streamUrl, long long startPositionTicks = 0,
|
|
||||||
const QVariant& sourceInfoVar = QVariant());
|
|
||||||
|
|
||||||
private:
|
|
||||||
void setMacSystemButtonsVisible(bool visible);
|
|
||||||
|
|
||||||
PlayerView* m_playerView = nullptr;
|
|
||||||
QWK::WidgetWindowAgent* m_windowAgent = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,352 +0,0 @@
|
|||||||
#include "searchcompleterpopup.h"
|
|
||||||
|
|
||||||
#include "../managers/thememanager.h"
|
|
||||||
#include "../tokens/ColorTokens.h"
|
|
||||||
#include <QAbstractItemView>
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QFontMetrics>
|
|
||||||
#include <QIcon>
|
|
||||||
#include <QLinearGradient>
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QPainterPath>
|
|
||||||
#include <QScrollBar>
|
|
||||||
#include <QStyle>
|
|
||||||
#include <QStyledItemDelegate>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
|
|
||||||
constexpr int kPopupItemHeight = 40;
|
|
||||||
constexpr int kPopupItemHorizontalInset = 4;
|
|
||||||
constexpr int kPopupItemVerticalInset = 3;
|
|
||||||
constexpr int kPopupItemRadius = 13;
|
|
||||||
constexpr int kPopupItemIconBubbleSize = 22;
|
|
||||||
constexpr int kPopupItemIconSize = 14;
|
|
||||||
constexpr int kPopupItemLeftPadding = 12;
|
|
||||||
constexpr int kPopupItemRightPadding = 12;
|
|
||||||
constexpr int kPopupItemContentGap = 10;
|
|
||||||
constexpr int kPopupPanelChromeHeight = 2;
|
|
||||||
|
|
||||||
struct PopupItemPalette
|
|
||||||
{
|
|
||||||
QColor backgroundTop;
|
|
||||||
QColor backgroundBottom;
|
|
||||||
QColor border;
|
|
||||||
QColor text;
|
|
||||||
QColor textHighlight;
|
|
||||||
QColor iconBubble;
|
|
||||||
QColor icon;
|
|
||||||
};
|
|
||||||
|
|
||||||
PopupItemPalette popupItemPalette(bool darkMode, bool hovered, bool selected)
|
|
||||||
{
|
|
||||||
using namespace ColorTokens;
|
|
||||||
const int level = selected ? 3 : (hovered ? 2 : 0);
|
|
||||||
const SearchTint::Surface s = SearchTint::surface(level, darkMode);
|
|
||||||
|
|
||||||
PopupItemPalette palette;
|
|
||||||
palette.backgroundTop = s.bgTop;
|
|
||||||
palette.backgroundBottom = s.bgBottom;
|
|
||||||
palette.border = s.border;
|
|
||||||
palette.text = s.text;
|
|
||||||
palette.textHighlight = darkMode ? Blue::k200 : Blue::k700;
|
|
||||||
palette.iconBubble = mix(s.bgTop, s.accent, darkMode ? 0.25 : 0.15);
|
|
||||||
palette.icon = s.accent;
|
|
||||||
return palette;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SearchCompleterItemDelegate : public QStyledItemDelegate
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit SearchCompleterItemDelegate(const SearchCompleterPopup *popup,
|
|
||||||
QObject *parent = nullptr)
|
|
||||||
: QStyledItemDelegate(parent), m_popup(popup)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize sizeHint(const QStyleOptionViewItem &option,
|
|
||||||
const QModelIndex &index) const override
|
|
||||||
{
|
|
||||||
Q_UNUSED(option);
|
|
||||||
Q_UNUSED(index);
|
|
||||||
return QSize(0, kPopupItemHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
void paint(QPainter *painter, const QStyleOptionViewItem &option,
|
|
||||||
const QModelIndex &index) const override
|
|
||||||
{
|
|
||||||
if (!painter)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString text = index.data(Qt::DisplayRole).toString().trimmed();
|
|
||||||
if (text.isEmpty())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool darkMode = ThemeManager::instance()->isDarkMode();
|
|
||||||
const bool selected = option.state & QStyle::State_Selected;
|
|
||||||
const bool hovered = option.state & QStyle::State_MouseOver;
|
|
||||||
const PopupItemPalette palette =
|
|
||||||
popupItemPalette(darkMode, hovered, selected);
|
|
||||||
|
|
||||||
painter->save();
|
|
||||||
painter->setRenderHint(QPainter::Antialiasing, true);
|
|
||||||
painter->setRenderHint(QPainter::TextAntialiasing, true);
|
|
||||||
painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
|
|
||||||
|
|
||||||
const QRect itemRect =
|
|
||||||
option.rect.adjusted(kPopupItemHorizontalInset,
|
|
||||||
kPopupItemVerticalInset,
|
|
||||||
-kPopupItemHorizontalInset,
|
|
||||||
-kPopupItemVerticalInset);
|
|
||||||
const QRectF cardRect =
|
|
||||||
QRectF(itemRect).adjusted(0.5, 0.5, -0.5, -0.5);
|
|
||||||
|
|
||||||
QPainterPath cardPath;
|
|
||||||
cardPath.addRoundedRect(cardRect, kPopupItemRadius, kPopupItemRadius);
|
|
||||||
|
|
||||||
QLinearGradient backgroundGradient(0, cardRect.top(), 0,
|
|
||||||
cardRect.bottom());
|
|
||||||
backgroundGradient.setColorAt(0.0, palette.backgroundTop);
|
|
||||||
backgroundGradient.setColorAt(1.0, palette.backgroundBottom);
|
|
||||||
painter->fillPath(cardPath, backgroundGradient);
|
|
||||||
painter->setPen(QPen(palette.border, 0.9));
|
|
||||||
painter->drawPath(cardPath);
|
|
||||||
|
|
||||||
const int iconBubbleY =
|
|
||||||
itemRect.top() + (itemRect.height() - kPopupItemIconBubbleSize) / 2;
|
|
||||||
const QRect iconBubbleRect(itemRect.left() + kPopupItemLeftPadding,
|
|
||||||
iconBubbleY,
|
|
||||||
kPopupItemIconBubbleSize,
|
|
||||||
kPopupItemIconBubbleSize);
|
|
||||||
QPainterPath iconBubblePath;
|
|
||||||
iconBubblePath.addEllipse(QRectF(iconBubbleRect).adjusted(
|
|
||||||
0.5, 0.5, -0.5, -0.5));
|
|
||||||
painter->fillPath(iconBubblePath, palette.iconBubble);
|
|
||||||
|
|
||||||
const QRect iconRect(
|
|
||||||
iconBubbleRect.left() +
|
|
||||||
(iconBubbleRect.width() - kPopupItemIconSize) / 2,
|
|
||||||
iconBubbleRect.top() +
|
|
||||||
(iconBubbleRect.height() - kPopupItemIconSize) / 2,
|
|
||||||
kPopupItemIconSize, kPopupItemIconSize);
|
|
||||||
ThemeManager::getAdaptiveIcon(
|
|
||||||
QStringLiteral(":/svg/dark/search.svg"), palette.icon)
|
|
||||||
.paint(painter, iconRect);
|
|
||||||
|
|
||||||
const QRect textRect(
|
|
||||||
iconBubbleRect.right() + kPopupItemContentGap,
|
|
||||||
itemRect.top(),
|
|
||||||
qMax(1, itemRect.right() - kPopupItemRightPadding -
|
|
||||||
(iconBubbleRect.right() + kPopupItemContentGap) + 1),
|
|
||||||
itemRect.height());
|
|
||||||
|
|
||||||
QFont textFont = option.font;
|
|
||||||
textFont.setPointSizeF(9.75);
|
|
||||||
textFont.setWeight(QFont::Medium);
|
|
||||||
|
|
||||||
QFont highlightFont = textFont;
|
|
||||||
highlightFont.setWeight(QFont::DemiBold);
|
|
||||||
|
|
||||||
const QFontMetrics textMetrics(textFont);
|
|
||||||
const QString displayText =
|
|
||||||
textMetrics.elidedText(text, Qt::ElideRight, textRect.width());
|
|
||||||
const QString highlightText =
|
|
||||||
m_popup ? m_popup->highlightText().trimmed() : QString();
|
|
||||||
const int highlightPos =
|
|
||||||
highlightText.isEmpty()
|
|
||||||
? -1
|
|
||||||
: text.indexOf(highlightText, 0, Qt::CaseInsensitive);
|
|
||||||
|
|
||||||
if (displayText != text || highlightPos < 0)
|
|
||||||
{
|
|
||||||
painter->setPen(palette.text);
|
|
||||||
painter->setFont(textFont);
|
|
||||||
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter,
|
|
||||||
displayText);
|
|
||||||
painter->restore();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString prefixText = text.left(highlightPos);
|
|
||||||
const QString matchedText = text.mid(highlightPos,
|
|
||||||
highlightText.size());
|
|
||||||
const QString suffixText =
|
|
||||||
text.mid(highlightPos + highlightText.size());
|
|
||||||
|
|
||||||
const QFontMetrics normalMetrics(textFont);
|
|
||||||
const QFontMetrics matchedMetrics(highlightFont);
|
|
||||||
const int prefixWidth = normalMetrics.horizontalAdvance(prefixText);
|
|
||||||
const int matchedWidth =
|
|
||||||
matchedMetrics.horizontalAdvance(matchedText);
|
|
||||||
const int totalTextWidth = prefixWidth + matchedWidth +
|
|
||||||
normalMetrics.horizontalAdvance(suffixText);
|
|
||||||
int drawX = textRect.left();
|
|
||||||
if (totalTextWidth < textRect.width())
|
|
||||||
{
|
|
||||||
drawX += 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int baselineY =
|
|
||||||
textRect.top() + (textRect.height() + normalMetrics.ascent() -
|
|
||||||
normalMetrics.descent()) /
|
|
||||||
2;
|
|
||||||
|
|
||||||
painter->setPen(palette.text);
|
|
||||||
painter->setFont(textFont);
|
|
||||||
painter->drawText(drawX, baselineY, prefixText);
|
|
||||||
drawX += prefixWidth;
|
|
||||||
|
|
||||||
painter->setPen(palette.textHighlight);
|
|
||||||
painter->setFont(highlightFont);
|
|
||||||
painter->drawText(drawX, baselineY, matchedText);
|
|
||||||
drawX += matchedWidth;
|
|
||||||
|
|
||||||
painter->setPen(palette.text);
|
|
||||||
painter->setFont(textFont);
|
|
||||||
painter->drawText(drawX, baselineY, suffixText);
|
|
||||||
painter->restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
const SearchCompleterPopup *m_popup = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchCompleterPopup::SearchCompleterPopup(QWidget *parent)
|
|
||||||
: QListView(parent)
|
|
||||||
{
|
|
||||||
setObjectName(QStringLiteral("SearchCompleterPopup"));
|
|
||||||
setViewMode(QListView::ListMode);
|
|
||||||
setMovement(QListView::Static);
|
|
||||||
setResizeMode(QListView::Adjust);
|
|
||||||
setMouseTracking(true);
|
|
||||||
setSpacing(0);
|
|
||||||
setUniformItemSizes(true);
|
|
||||||
setAlternatingRowColors(false);
|
|
||||||
setEditTriggers(QAbstractItemView::NoEditTriggers);
|
|
||||||
setSelectionBehavior(QAbstractItemView::SelectRows);
|
|
||||||
setSelectionMode(QAbstractItemView::SingleSelection);
|
|
||||||
setTextElideMode(Qt::ElideRight);
|
|
||||||
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
||||||
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
||||||
setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
|
||||||
setFrameShape(QFrame::NoFrame);
|
|
||||||
setCursor(Qt::PointingHandCursor);
|
|
||||||
setAttribute(Qt::WA_TranslucentBackground, true);
|
|
||||||
setAttribute(Qt::WA_ShowWithoutActivating, true);
|
|
||||||
setAutoFillBackground(false);
|
|
||||||
setWindowFlags(Qt::Popup | Qt::FramelessWindowHint |
|
|
||||||
Qt::NoDropShadowWindowHint);
|
|
||||||
|
|
||||||
if (viewport())
|
|
||||||
{
|
|
||||||
viewport()->setAttribute(Qt::WA_Hover, true);
|
|
||||||
viewport()->setMouseTracking(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto *bar = verticalScrollBar())
|
|
||||||
{
|
|
||||||
bar->setSingleStep(16);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_itemDelegate = new SearchCompleterItemDelegate(this, this);
|
|
||||||
setItemDelegate(m_itemDelegate);
|
|
||||||
|
|
||||||
connect(ThemeManager::instance(), &ThemeManager::themeChanged, this,
|
|
||||||
[this](ThemeManager::Theme) {
|
|
||||||
if (viewport())
|
|
||||||
{
|
|
||||||
viewport()->update();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void SearchCompleterPopup::setHighlightText(QString text)
|
|
||||||
{
|
|
||||||
text = text.trimmed();
|
|
||||||
if (m_highlightText == text)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_highlightText = std::move(text);
|
|
||||||
if (viewport())
|
|
||||||
{
|
|
||||||
viewport()->update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QString SearchCompleterPopup::highlightText() const
|
|
||||||
{
|
|
||||||
return m_highlightText;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SearchCompleterPopup::setModel(QAbstractItemModel *model)
|
|
||||||
{
|
|
||||||
QListView::setModel(model);
|
|
||||||
ensureDelegate();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SearchCompleterPopup::showEvent(QShowEvent *event)
|
|
||||||
{
|
|
||||||
ensureDelegate();
|
|
||||||
QListView::showEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SearchCompleterPopup::syncWidthToAnchor(const QWidget *anchor)
|
|
||||||
{
|
|
||||||
if (!anchor)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFixedSize(anchor->width(), desiredPopupHeight());
|
|
||||||
}
|
|
||||||
|
|
||||||
void SearchCompleterPopup::setMaxVisibleRows(int rows)
|
|
||||||
{
|
|
||||||
m_maxVisibleRows = qMax(1, rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
QRect SearchCompleterPopup::popupRectForAnchor(const QWidget *anchor,
|
|
||||||
int verticalOffset) const
|
|
||||||
{
|
|
||||||
if (!anchor)
|
|
||||||
{
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return QRect(0, anchor->height() + verticalOffset, anchor->width(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
int SearchCompleterPopup::sizeHintForRow(int row) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(row);
|
|
||||||
return kPopupItemHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
int SearchCompleterPopup::desiredPopupHeight() const
|
|
||||||
{
|
|
||||||
const int rowCount = model() ? model()->rowCount(rootIndex()) : 0;
|
|
||||||
const int visibleRows = qMax(1, qMin(m_maxVisibleRows, rowCount));
|
|
||||||
return visibleRows * kPopupItemHeight + kPopupPanelChromeHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SearchCompleterPopup::ensureDelegate()
|
|
||||||
{
|
|
||||||
if (!m_itemDelegate)
|
|
||||||
{
|
|
||||||
m_itemDelegate = new SearchCompleterItemDelegate(this, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (itemDelegate() != m_itemDelegate)
|
|
||||||
{
|
|
||||||
setItemDelegate(m_itemDelegate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
#ifndef SEARCHCOMPLETERPOPUP_H
|
|
||||||
#define SEARCHCOMPLETERPOPUP_H
|
|
||||||
|
|
||||||
#include <QListView>
|
|
||||||
#include <QRect>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
class QAbstractItemModel;
|
|
||||||
class QShowEvent;
|
|
||||||
class QStyledItemDelegate;
|
|
||||||
class QWidget;
|
|
||||||
|
|
||||||
class SearchCompleterPopup : public QListView
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit SearchCompleterPopup(QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
void setHighlightText(QString text);
|
|
||||||
QString highlightText() const;
|
|
||||||
void syncWidthToAnchor(const QWidget *anchor);
|
|
||||||
void setMaxVisibleRows(int rows);
|
|
||||||
QRect popupRectForAnchor(const QWidget *anchor,
|
|
||||||
int verticalOffset = 3) const;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void setModel(QAbstractItemModel *model) override;
|
|
||||||
void showEvent(QShowEvent *event) override;
|
|
||||||
int sizeHintForRow(int row) const override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
int desiredPopupHeight() const;
|
|
||||||
void ensureDelegate();
|
|
||||||
|
|
||||||
QString m_highlightText;
|
|
||||||
QStyledItemDelegate *m_itemDelegate = nullptr;
|
|
||||||
int m_maxVisibleRows = 8;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,552 +0,0 @@
|
|||||||
#include "searchhistorychip.h"
|
|
||||||
|
|
||||||
#include "../managers/thememanager.h"
|
|
||||||
#include "../../qEmbyCore/config/config_keys.h"
|
|
||||||
#include "../../qEmbyCore/config/configstore.h"
|
|
||||||
#include <QColor>
|
|
||||||
#include <QEnterEvent>
|
|
||||||
#include <QEasingCurve>
|
|
||||||
#include <QFontMetrics>
|
|
||||||
#include <QLinearGradient>
|
|
||||||
#include <QMouseEvent>
|
|
||||||
#include <QPaintEvent>
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QPainterPath>
|
|
||||||
#include <QVariantAnimation>
|
|
||||||
#include <utility>
|
|
||||||
#include "../tokens/AnimationTokens.h"
|
|
||||||
#include "../tokens/ColorTokens.h"
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
|
|
||||||
constexpr int kChipHeight = 26;
|
|
||||||
constexpr int kChipBodyHeight = 20;
|
|
||||||
constexpr int kChipTopOverflow = 5;
|
|
||||||
constexpr int kChipSideOverflow = 4;
|
|
||||||
constexpr int kChipMinWidth = 46;
|
|
||||||
constexpr int kChipMaxWidth = 158;
|
|
||||||
constexpr int kChipHorizontalPadding = 10;
|
|
||||||
constexpr int kChipBadgeTop = 0;
|
|
||||||
constexpr int kChipBadgeHeight = 12;
|
|
||||||
constexpr int kChipRemoveTop = 0;
|
|
||||||
constexpr int kChipRemoveButtonSize = 12;
|
|
||||||
constexpr int kChipRemoveHoverSize = 12;
|
|
||||||
constexpr int kChipHoverDecorFadeInDurationMs = AnimationTokens::DurationMs::kQuick;
|
|
||||||
constexpr int kChipHoverDecorFadeOutDurationMs = AnimationTokens::DurationMs::kMicro;
|
|
||||||
constexpr qreal kChipHoverDecorMinScale = 0.9;
|
|
||||||
constexpr qreal kChipHoverDecorMaxShiftY = 1.8;
|
|
||||||
constexpr qreal kChipRemoveDecorDelayProgress = 0.18;
|
|
||||||
|
|
||||||
struct ChipPalette
|
|
||||||
{
|
|
||||||
QColor backgroundTop;
|
|
||||||
QColor backgroundBottom;
|
|
||||||
QColor border;
|
|
||||||
QColor text;
|
|
||||||
QColor badgeBackground;
|
|
||||||
QColor badgeBorder;
|
|
||||||
QColor badgeText;
|
|
||||||
QColor removeBackground;
|
|
||||||
QColor removeIcon;
|
|
||||||
};
|
|
||||||
|
|
||||||
QColor withAlpha(QColor color, int alpha)
|
|
||||||
{
|
|
||||||
color.setAlpha(alpha);
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
int scaledAlpha(int alpha, qreal progress)
|
|
||||||
{
|
|
||||||
return qBound(0, qRound(alpha * progress), 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
qreal delayedProgress(qreal progress, qreal delay)
|
|
||||||
{
|
|
||||||
if (progress <= delay)
|
|
||||||
{
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delay >= 0.999)
|
|
||||||
{
|
|
||||||
return 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return qBound(0.0, (progress - delay) / (1.0 - delay), 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
QRectF animatedDecorationRect(const QRect &rect, qreal progress)
|
|
||||||
{
|
|
||||||
const qreal scale =
|
|
||||||
kChipHoverDecorMinScale + (1.0 - kChipHoverDecorMinScale) * progress;
|
|
||||||
const qreal width = rect.width() * scale;
|
|
||||||
const qreal height = rect.height() * scale;
|
|
||||||
const QPointF center(rect.center().x() + 0.5,
|
|
||||||
rect.center().y() + 0.5 +
|
|
||||||
(1.0 - progress) * kChipHoverDecorMaxShiftY);
|
|
||||||
return QRectF(center.x() - width / 2.0, center.y() - height / 2.0,
|
|
||||||
width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
QFont chipTextFont(const QWidget *widget)
|
|
||||||
{
|
|
||||||
QFont font = widget->font();
|
|
||||||
font.setPixelSize(qMax(11, qRound(12.0 * ThemeManager::fontScaleFactor())));
|
|
||||||
font.setWeight(QFont::Medium);
|
|
||||||
return font;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFont chipBadgeFont(const QWidget *widget)
|
|
||||||
{
|
|
||||||
QFont font = widget->font();
|
|
||||||
font.setPixelSize(qMax(7, qRound(8.0 * ThemeManager::fontScaleFactor())));
|
|
||||||
font.setWeight(QFont::Bold);
|
|
||||||
return font;
|
|
||||||
}
|
|
||||||
|
|
||||||
int heatTintLevel(const QString &heatLevel)
|
|
||||||
{
|
|
||||||
if (heatLevel == QLatin1String("top"))
|
|
||||||
{
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
if (heatLevel == QLatin1String("high"))
|
|
||||||
{
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
if (heatLevel == QLatin1String("mid"))
|
|
||||||
{
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChipPalette basePalette(const QString &heatLevel, bool darkMode)
|
|
||||||
{
|
|
||||||
using namespace ColorTokens;
|
|
||||||
const SearchTint::Surface s =
|
|
||||||
SearchTint::surface(heatTintLevel(heatLevel), darkMode);
|
|
||||||
|
|
||||||
ChipPalette palette;
|
|
||||||
palette.backgroundTop = s.bgTop;
|
|
||||||
palette.backgroundBottom = s.bgBottom;
|
|
||||||
palette.border = s.border;
|
|
||||||
palette.text = s.text;
|
|
||||||
palette.badgeBackground = mix(s.bgTop, s.accent, darkMode ? 0.35 : 0.20);
|
|
||||||
palette.badgeBorder = mix(s.border, s.accent, 0.5);
|
|
||||||
palette.badgeText = darkMode ? Slate::k50 : Blue::k700;
|
|
||||||
palette.removeBackground = mix(s.bgTop, s.border, 0.5);
|
|
||||||
palette.removeIcon = darkMode ? Slate::k200 : Slate::k500;
|
|
||||||
return palette;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChipPalette statePalette(const QString &heatLevel, bool darkMode, bool hovered, bool pressed)
|
|
||||||
{
|
|
||||||
ChipPalette palette = basePalette(heatLevel, darkMode);
|
|
||||||
|
|
||||||
if (hovered)
|
|
||||||
{
|
|
||||||
palette.backgroundTop = palette.backgroundTop.lighter(darkMode ? 116 : 103);
|
|
||||||
palette.backgroundBottom = palette.backgroundBottom.lighter(darkMode ? 118 : 104);
|
|
||||||
palette.border = palette.border.lighter(darkMode ? 128 : 108);
|
|
||||||
palette.badgeBackground = palette.badgeBackground.lighter(darkMode ? 108 : 103);
|
|
||||||
palette.badgeBorder = palette.badgeBorder.lighter(darkMode ? 122 : 108);
|
|
||||||
palette.removeBackground = palette.removeBackground.lighter(darkMode ? 110 : 103);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pressed)
|
|
||||||
{
|
|
||||||
palette.backgroundTop = palette.backgroundTop.darker(darkMode ? 112 : 104);
|
|
||||||
palette.backgroundBottom = palette.backgroundBottom.darker(darkMode ? 116 : 106);
|
|
||||||
palette.border = palette.border.darker(darkMode ? 112 : 108);
|
|
||||||
palette.badgeBackground = palette.badgeBackground.darker(darkMode ? 106 : 102);
|
|
||||||
palette.removeBackground = palette.removeBackground.darker(darkMode ? 106 : 102);
|
|
||||||
}
|
|
||||||
|
|
||||||
return palette;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchHistoryChip::SearchHistoryChip(QWidget *parent)
|
|
||||||
: QAbstractButton(parent)
|
|
||||||
{
|
|
||||||
setObjectName(QStringLiteral("SearchHistoryChip"));
|
|
||||||
setCursor(Qt::PointingHandCursor);
|
|
||||||
setFocusPolicy(Qt::NoFocus);
|
|
||||||
setAttribute(Qt::WA_Hover, true);
|
|
||||||
setMouseTracking(true);
|
|
||||||
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
||||||
setFixedHeight(kChipHeight);
|
|
||||||
|
|
||||||
m_hoverDecorAnimation = new QVariantAnimation(this);
|
|
||||||
m_hoverDecorAnimation->setStartValue(0.0);
|
|
||||||
m_hoverDecorAnimation->setEndValue(1.0);
|
|
||||||
m_hoverDecorAnimation->setDuration(kChipHoverDecorFadeInDurationMs);
|
|
||||||
m_hoverDecorAnimation->setEasingCurve(AnimationTokens::Curve::enter());
|
|
||||||
connect(m_hoverDecorAnimation, &QVariantAnimation::valueChanged, this,
|
|
||||||
[this](const QVariant &value) {
|
|
||||||
m_hoverDecorProgress = value.toReal();
|
|
||||||
update();
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(ConfigStore::instance(), &ConfigStore::valueChanged, this,
|
|
||||||
[this](const QString &key, const QVariant &) {
|
|
||||||
if (key == ConfigKeys::FontSize)
|
|
||||||
{
|
|
||||||
updateMetrics();
|
|
||||||
}
|
|
||||||
else if (key == ConfigKeys::UiAnimations)
|
|
||||||
{
|
|
||||||
if (m_hoverDecorAnimation)
|
|
||||||
{
|
|
||||||
m_hoverDecorAnimation->stop();
|
|
||||||
}
|
|
||||||
m_hoverDecorProgress = shouldShowHoverDecorations() ? 1.0 : 0.0;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
updateMetrics();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SearchHistoryChip::setTerm(QString term)
|
|
||||||
{
|
|
||||||
term = term.trimmed();
|
|
||||||
if (m_term == term)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_term = std::move(term);
|
|
||||||
setToolTip(m_term);
|
|
||||||
updateMetrics();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString SearchHistoryChip::term() const
|
|
||||||
{
|
|
||||||
return m_term;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SearchHistoryChip::setSearchCount(int count)
|
|
||||||
{
|
|
||||||
const int safeCount = qMax(1, count);
|
|
||||||
if (m_searchCount == safeCount)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_searchCount = safeCount;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
int SearchHistoryChip::searchCount() const
|
|
||||||
{
|
|
||||||
return m_searchCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SearchHistoryChip::setHeatLevel(const QString &level)
|
|
||||||
{
|
|
||||||
const QString normalizedLevel = level.trimmed().isEmpty() ? QStringLiteral("low")
|
|
||||||
: level.trimmed();
|
|
||||||
if (m_heatLevel == normalizedLevel)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_heatLevel = normalizedLevel;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString SearchHistoryChip::heatLevel() const
|
|
||||||
{
|
|
||||||
return m_heatLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize SearchHistoryChip::sizeHint() const
|
|
||||||
{
|
|
||||||
return {m_cachedWidth, kChipHeight};
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize SearchHistoryChip::minimumSizeHint() const
|
|
||||||
{
|
|
||||||
return sizeHint();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SearchHistoryChip::hitButton(const QPoint &pos) const
|
|
||||||
{
|
|
||||||
return bodyRect().contains(pos) || badgeRect().contains(pos) ||
|
|
||||||
removeButtonRect().contains(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SearchHistoryChip::paintEvent(QPaintEvent *event)
|
|
||||||
{
|
|
||||||
Q_UNUSED(event);
|
|
||||||
|
|
||||||
QPainter painter(this);
|
|
||||||
painter.setRenderHint(QPainter::Antialiasing);
|
|
||||||
painter.setRenderHint(QPainter::TextAntialiasing);
|
|
||||||
|
|
||||||
const ChipPalette palette = statePalette(m_heatLevel,
|
|
||||||
ThemeManager::instance()->isDarkMode(),
|
|
||||||
underMouse(), isDown());
|
|
||||||
const qreal decorProgress = qBound(0.0, m_hoverDecorProgress, 1.0);
|
|
||||||
const qreal removeBackgroundProgress = decorProgress;
|
|
||||||
const qreal removeIconProgress =
|
|
||||||
delayedProgress(decorProgress, kChipRemoveDecorDelayProgress);
|
|
||||||
|
|
||||||
const QRect chipBody = bodyRect();
|
|
||||||
const QRectF outerRect = QRectF(chipBody).adjusted(0.5, 0.5, -0.5, -0.5);
|
|
||||||
const qreal radius = outerRect.height() / 2.0;
|
|
||||||
|
|
||||||
QPainterPath chipPath;
|
|
||||||
chipPath.addRoundedRect(outerRect, radius, radius);
|
|
||||||
|
|
||||||
QLinearGradient backgroundGradient(0, outerRect.top(), 0, outerRect.bottom());
|
|
||||||
backgroundGradient.setColorAt(0.0, palette.backgroundTop);
|
|
||||||
backgroundGradient.setColorAt(1.0, palette.backgroundBottom);
|
|
||||||
|
|
||||||
painter.fillPath(chipPath, backgroundGradient);
|
|
||||||
painter.setPen(QPen(palette.border, 0.85));
|
|
||||||
painter.drawPath(chipPath);
|
|
||||||
|
|
||||||
painter.setPen(palette.text);
|
|
||||||
painter.setFont(chipTextFont(this));
|
|
||||||
|
|
||||||
const QRect badgeBox = badgeRect();
|
|
||||||
const QRect removeRect = removeButtonRect();
|
|
||||||
const QRect textRect(chipBody.left() + kChipHorizontalPadding,
|
|
||||||
chipBody.top() + 1,
|
|
||||||
qMax(1, chipBody.width() - kChipHorizontalPadding * 2),
|
|
||||||
chipBody.height() - 2);
|
|
||||||
|
|
||||||
const QFontMetrics textMetrics(chipTextFont(this));
|
|
||||||
const QString displayText =
|
|
||||||
textMetrics.elidedText(m_term, Qt::ElideRight, textRect.width());
|
|
||||||
const bool shouldCenterText =
|
|
||||||
textMetrics.horizontalAdvance(displayText) <= textRect.width();
|
|
||||||
painter.drawText(textRect,
|
|
||||||
(shouldCenterText ? Qt::AlignHCenter : Qt::AlignLeft) |
|
|
||||||
Qt::AlignVCenter,
|
|
||||||
displayText);
|
|
||||||
|
|
||||||
if (decorProgress <= 0.001)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QRectF animatedBadgeRect = animatedDecorationRect(badgeBox, decorProgress);
|
|
||||||
QPainterPath badgePath;
|
|
||||||
badgePath.addEllipse(animatedBadgeRect.adjusted(0.5, 0.5, -0.5, -0.5));
|
|
||||||
painter.fillPath(badgePath,
|
|
||||||
withAlpha(palette.badgeBackground,
|
|
||||||
scaledAlpha(235, decorProgress)));
|
|
||||||
painter.setPen(QPen(withAlpha(palette.badgeBorder,
|
|
||||||
scaledAlpha(190, decorProgress)),
|
|
||||||
0.85));
|
|
||||||
painter.drawPath(badgePath);
|
|
||||||
|
|
||||||
painter.setFont(chipBadgeFont(this));
|
|
||||||
painter.setPen(withAlpha(palette.badgeText, scaledAlpha(255, decorProgress)));
|
|
||||||
painter.drawText(animatedBadgeRect, Qt::AlignCenter, formattedCount());
|
|
||||||
|
|
||||||
if (removeBackgroundProgress <= 0.001)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QRect hoverRect(removeRect.center().x() - kChipRemoveHoverSize / 2,
|
|
||||||
removeRect.center().y() - kChipRemoveHoverSize / 2,
|
|
||||||
kChipRemoveHoverSize, kChipRemoveHoverSize);
|
|
||||||
const QRectF animatedRemoveRect =
|
|
||||||
animatedDecorationRect(hoverRect, removeBackgroundProgress);
|
|
||||||
QPainterPath removePath;
|
|
||||||
removePath.addEllipse(animatedRemoveRect.adjusted(0.5, 0.5, -0.5, -0.5));
|
|
||||||
painter.fillPath(removePath, withAlpha(palette.removeBackground,
|
|
||||||
scaledAlpha((m_removeHovered || m_removePressed) ? 226
|
|
||||||
: 188,
|
|
||||||
removeBackgroundProgress)));
|
|
||||||
painter.setPen(QPen(withAlpha(palette.border,
|
|
||||||
scaledAlpha(175, removeBackgroundProgress)),
|
|
||||||
0.85));
|
|
||||||
painter.drawPath(removePath);
|
|
||||||
|
|
||||||
if (removeIconProgress <= 0.001)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
painter.setPen(QPen(withAlpha(palette.removeIcon,
|
|
||||||
scaledAlpha(255, removeIconProgress)),
|
|
||||||
1.15, Qt::SolidLine, Qt::RoundCap));
|
|
||||||
const QPointF removeCenter = animatedRemoveRect.center();
|
|
||||||
const qreal removeHalfSize = 2.55;
|
|
||||||
painter.drawLine(QPointF(removeCenter.x() - removeHalfSize,
|
|
||||||
removeCenter.y() - removeHalfSize),
|
|
||||||
QPointF(removeCenter.x() + removeHalfSize,
|
|
||||||
removeCenter.y() + removeHalfSize));
|
|
||||||
painter.drawLine(QPointF(removeCenter.x() - removeHalfSize,
|
|
||||||
removeCenter.y() + removeHalfSize),
|
|
||||||
QPointF(removeCenter.x() + removeHalfSize,
|
|
||||||
removeCenter.y() - removeHalfSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
void SearchHistoryChip::enterEvent(QEnterEvent *event)
|
|
||||||
{
|
|
||||||
QAbstractButton::enterEvent(event);
|
|
||||||
syncHoverDecorationVisibility();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SearchHistoryChip::leaveEvent(QEvent *event)
|
|
||||||
{
|
|
||||||
QAbstractButton::leaveEvent(event);
|
|
||||||
m_removeHovered = false;
|
|
||||||
m_removePressed = false;
|
|
||||||
syncHoverDecorationVisibility();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SearchHistoryChip::mouseMoveEvent(QMouseEvent *event)
|
|
||||||
{
|
|
||||||
updateRemoveHover(event->position().toPoint());
|
|
||||||
QAbstractButton::mouseMoveEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SearchHistoryChip::mousePressEvent(QMouseEvent *event)
|
|
||||||
{
|
|
||||||
if (event->button() == Qt::LeftButton &&
|
|
||||||
removeButtonRect().contains(event->position().toPoint()))
|
|
||||||
{
|
|
||||||
m_removePressed = true;
|
|
||||||
m_removeHovered = true;
|
|
||||||
syncHoverDecorationVisibility();
|
|
||||||
event->accept();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_removePressed = false;
|
|
||||||
QAbstractButton::mousePressEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SearchHistoryChip::mouseReleaseEvent(QMouseEvent *event)
|
|
||||||
{
|
|
||||||
if (m_removePressed)
|
|
||||||
{
|
|
||||||
const bool triggered = event->button() == Qt::LeftButton &&
|
|
||||||
removeButtonRect().contains(event->position().toPoint());
|
|
||||||
m_removePressed = false;
|
|
||||||
updateRemoveHover(event->position().toPoint());
|
|
||||||
syncHoverDecorationVisibility();
|
|
||||||
event->accept();
|
|
||||||
|
|
||||||
if (triggered && !m_term.isEmpty())
|
|
||||||
{
|
|
||||||
Q_EMIT removeRequested(m_term);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QAbstractButton::mouseReleaseEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SearchHistoryChip::syncHoverDecorationVisibility()
|
|
||||||
{
|
|
||||||
const bool visible = shouldShowHoverDecorations();
|
|
||||||
const qreal targetProgress = visible ? 1.0 : 0.0;
|
|
||||||
if (m_hoverDecorVisible == visible)
|
|
||||||
{
|
|
||||||
if (m_hoverDecorAnimation &&
|
|
||||||
m_hoverDecorAnimation->state() == QAbstractAnimation::Running)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (qAbs(m_hoverDecorProgress - targetProgress) < 0.001)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_hoverDecorVisible = visible;
|
|
||||||
|
|
||||||
const bool reduceAnimations =
|
|
||||||
ConfigStore::instance()->get<bool>(ConfigKeys::UiAnimations, false);
|
|
||||||
if (!m_hoverDecorAnimation || reduceAnimations)
|
|
||||||
{
|
|
||||||
if (m_hoverDecorAnimation)
|
|
||||||
{
|
|
||||||
m_hoverDecorAnimation->stop();
|
|
||||||
}
|
|
||||||
m_hoverDecorProgress = targetProgress;
|
|
||||||
update();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_hoverDecorAnimation->stop();
|
|
||||||
m_hoverDecorAnimation->setDuration(visible ? kChipHoverDecorFadeInDurationMs
|
|
||||||
: kChipHoverDecorFadeOutDurationMs);
|
|
||||||
m_hoverDecorAnimation->setEasingCurve(visible ? AnimationTokens::Curve::enter()
|
|
||||||
: AnimationTokens::Curve::exit());
|
|
||||||
m_hoverDecorAnimation->setStartValue(m_hoverDecorProgress);
|
|
||||||
m_hoverDecorAnimation->setEndValue(targetProgress);
|
|
||||||
m_hoverDecorAnimation->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SearchHistoryChip::shouldShowHoverDecorations() const
|
|
||||||
{
|
|
||||||
return underMouse() || m_removeHovered || m_removePressed;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SearchHistoryChip::updateRemoveHover(const QPoint &pos)
|
|
||||||
{
|
|
||||||
const bool hovered = removeButtonRect().contains(pos);
|
|
||||||
if (m_removeHovered == hovered)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_removeHovered = hovered;
|
|
||||||
syncHoverDecorationVisibility();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SearchHistoryChip::updateMetrics()
|
|
||||||
{
|
|
||||||
ensurePolished();
|
|
||||||
|
|
||||||
const QFontMetrics metrics(chipTextFont(this));
|
|
||||||
const int naturalTextWidth = metrics.horizontalAdvance(m_term);
|
|
||||||
m_cachedWidth = qBound(kChipMinWidth,
|
|
||||||
naturalTextWidth + kChipHorizontalPadding * 2 +
|
|
||||||
kChipSideOverflow * 2 + 2,
|
|
||||||
kChipMaxWidth);
|
|
||||||
|
|
||||||
setFixedSize(m_cachedWidth, kChipHeight);
|
|
||||||
updateGeometry();
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString SearchHistoryChip::formattedCount() const
|
|
||||||
{
|
|
||||||
return (m_searchCount > 999) ? QStringLiteral("999+")
|
|
||||||
: QString::number(qMax(1, m_searchCount));
|
|
||||||
}
|
|
||||||
|
|
||||||
QRect SearchHistoryChip::bodyRect() const
|
|
||||||
{
|
|
||||||
return QRect(kChipSideOverflow, kChipTopOverflow,
|
|
||||||
qMax(1, width() - kChipSideOverflow * 2), kChipBodyHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
QRect SearchHistoryChip::removeButtonRect() const
|
|
||||||
{
|
|
||||||
return QRect(width() - kChipRemoveButtonSize - 1,
|
|
||||||
kChipRemoveTop, kChipRemoveButtonSize, kChipRemoveButtonSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
QRect SearchHistoryChip::badgeRect() const
|
|
||||||
{
|
|
||||||
const QFontMetrics metrics(chipBadgeFont(this));
|
|
||||||
const int badgeWidth = qBound(kChipBadgeHeight,
|
|
||||||
metrics.horizontalAdvance(formattedCount()) + 6, 18);
|
|
||||||
return QRect(1, kChipBadgeTop, badgeWidth, kChipBadgeHeight);
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
#ifndef SEARCHHISTORYCHIP_H
|
|
||||||
#define SEARCHHISTORYCHIP_H
|
|
||||||
|
|
||||||
#include <QAbstractButton>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
class QEnterEvent;
|
|
||||||
class QEvent;
|
|
||||||
class QMouseEvent;
|
|
||||||
class QPaintEvent;
|
|
||||||
class QPoint;
|
|
||||||
class QVariantAnimation;
|
|
||||||
|
|
||||||
class SearchHistoryChip : public QAbstractButton
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit SearchHistoryChip(QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
void setTerm(QString term);
|
|
||||||
QString term() const;
|
|
||||||
|
|
||||||
void setSearchCount(int count);
|
|
||||||
int searchCount() const;
|
|
||||||
|
|
||||||
void setHeatLevel(const QString &level);
|
|
||||||
QString heatLevel() const;
|
|
||||||
|
|
||||||
QSize sizeHint() const override;
|
|
||||||
QSize minimumSizeHint() const override;
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void removeRequested(const QString &term);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool hitButton(const QPoint &pos) const override;
|
|
||||||
void paintEvent(QPaintEvent *event) override;
|
|
||||||
void enterEvent(QEnterEvent *event) override;
|
|
||||||
void leaveEvent(QEvent *event) override;
|
|
||||||
void mouseMoveEvent(QMouseEvent *event) override;
|
|
||||||
void mousePressEvent(QMouseEvent *event) override;
|
|
||||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void updateMetrics();
|
|
||||||
void syncHoverDecorationVisibility();
|
|
||||||
bool shouldShowHoverDecorations() const;
|
|
||||||
void updateRemoveHover(const QPoint &pos);
|
|
||||||
QRect bodyRect() const;
|
|
||||||
QRect removeButtonRect() const;
|
|
||||||
QRect badgeRect() const;
|
|
||||||
QString formattedCount() const;
|
|
||||||
|
|
||||||
QString m_term;
|
|
||||||
QString m_heatLevel = QStringLiteral("low");
|
|
||||||
int m_searchCount = 1;
|
|
||||||
int m_cachedWidth = 40;
|
|
||||||
bool m_removeHovered = false;
|
|
||||||
bool m_removePressed = false;
|
|
||||||
bool m_hoverDecorVisible = false;
|
|
||||||
qreal m_hoverDecorProgress = 0.0;
|
|
||||||
QVariantAnimation *m_hoverDecorAnimation = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,95 +0,0 @@
|
|||||||
#ifndef SEARCHHISTORYPOPUP_H
|
|
||||||
#define SEARCHHISTORYPOPUP_H
|
|
||||||
|
|
||||||
#include "../managers/searchhistorymanager.h"
|
|
||||||
#include <QFrame>
|
|
||||||
#include <QList>
|
|
||||||
|
|
||||||
class FlowLayout;
|
|
||||||
class QGraphicsOpacityEffect;
|
|
||||||
class QScrollArea;
|
|
||||||
class QToolButton;
|
|
||||||
class QVariantAnimation;
|
|
||||||
class QWidget;
|
|
||||||
|
|
||||||
class SearchHistoryPopup : public QFrame {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit SearchHistoryPopup(QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
void setEntries(QList<SearchHistoryManager::SearchHistoryEntry> entries);
|
|
||||||
bool hasContent() const;
|
|
||||||
void showBelow(QWidget *anchor);
|
|
||||||
void dismiss(bool immediate = false);
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void termActivated(const QString &term);
|
|
||||||
void clearHistoryRequested();
|
|
||||||
void removeHistoryTermRequested(const QString &term);
|
|
||||||
|
|
||||||
private:
|
|
||||||
enum class SortMode {
|
|
||||||
Recent,
|
|
||||||
Count
|
|
||||||
};
|
|
||||||
|
|
||||||
void rebuildChips(bool preserveScrollPosition = false);
|
|
||||||
int resolvedCardWidth() const;
|
|
||||||
void relayoutPopup(int preferredCardWidth = -1);
|
|
||||||
void repositionToAnchor();
|
|
||||||
void resetScrollPosition();
|
|
||||||
void stopHeightAnimation();
|
|
||||||
void stopSortTransitionAnimation();
|
|
||||||
void stopVisibilityAnimation();
|
|
||||||
void syncReducedAnimationMode();
|
|
||||||
void applyVisibilityState(qreal opacity, int offsetY);
|
|
||||||
void applyAnimatedHeights(int scrollHeight, int bodyHeight, int popupHeight);
|
|
||||||
void updateLayoutMetrics(int cardWidth);
|
|
||||||
void refreshControls();
|
|
||||||
void updateControlIcons();
|
|
||||||
void setClearArmed(bool armed);
|
|
||||||
void setExpanded(bool expanded);
|
|
||||||
void setSortMode(SortMode mode);
|
|
||||||
QList<SearchHistoryManager::SearchHistoryEntry> sortedEntries() const;
|
|
||||||
static QString heatLevelForCount(int count);
|
|
||||||
|
|
||||||
QFrame *m_card = nullptr;
|
|
||||||
QWidget *m_body = nullptr;
|
|
||||||
QWidget *m_header = nullptr;
|
|
||||||
QFrame *m_headerDivider = nullptr;
|
|
||||||
QToolButton *m_recentButton = nullptr;
|
|
||||||
QToolButton *m_hotButton = nullptr;
|
|
||||||
QToolButton *m_clearButton = nullptr;
|
|
||||||
QToolButton *m_expandButton = nullptr;
|
|
||||||
QScrollArea *m_scrollArea = nullptr;
|
|
||||||
QWidget *m_flowHost = nullptr;
|
|
||||||
FlowLayout *m_flowLayout = nullptr;
|
|
||||||
QGraphicsOpacityEffect *m_cardOpacityEffect = nullptr;
|
|
||||||
QGraphicsOpacityEffect *m_flowOpacityEffect = nullptr;
|
|
||||||
QVariantAnimation *m_heightAnimation = nullptr;
|
|
||||||
QVariantAnimation *m_sortTransitionAnimation = nullptr;
|
|
||||||
QVariantAnimation *m_visibilityAnimation = nullptr;
|
|
||||||
QWidget *m_anchor = nullptr;
|
|
||||||
int m_lastCardWidth = 0;
|
|
||||||
int m_animationStartScrollHeight = 0;
|
|
||||||
int m_animationStartBodyHeight = 0;
|
|
||||||
int m_animationStartPopupHeight = 0;
|
|
||||||
int m_animationTargetScrollHeight = 0;
|
|
||||||
int m_animationTargetBodyHeight = 0;
|
|
||||||
int m_animationTargetPopupHeight = 0;
|
|
||||||
qreal m_visibilityStartOpacity = 1.0;
|
|
||||||
qreal m_visibilityTargetOpacity = 1.0;
|
|
||||||
int m_visibilityOffsetY = 0;
|
|
||||||
int m_visibilityStartOffsetY = 0;
|
|
||||||
int m_visibilityTargetOffsetY = 0;
|
|
||||||
bool m_visibilityHiding = false;
|
|
||||||
bool m_animateNextRelayout = false;
|
|
||||||
bool m_sortTransitionRebuilt = false;
|
|
||||||
|
|
||||||
QList<SearchHistoryManager::SearchHistoryEntry> m_entries;
|
|
||||||
SortMode m_sortMode = SortMode::Recent;
|
|
||||||
bool m_expanded = false;
|
|
||||||
bool m_clearArmed = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
#include "seektimebubble.h"
|
|
||||||
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
#include "../utils/fadehelper.h"
|
|
||||||
|
|
||||||
SeekTimeBubble::SeekTimeBubble(QWidget *parent)
|
|
||||||
: QWidget(parent)
|
|
||||||
{
|
|
||||||
setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
||||||
setVisible(false);
|
|
||||||
|
|
||||||
auto *layout = new QVBoxLayout(this);
|
|
||||||
layout->setContentsMargins(10, 4, 10, 4);
|
|
||||||
layout->setSpacing(4);
|
|
||||||
|
|
||||||
m_thumbSlot = new QLabel(this);
|
|
||||||
m_thumbSlot->setObjectName("seekBubbleThumb");
|
|
||||||
m_thumbSlot->setVisible(false);
|
|
||||||
layout->addWidget(m_thumbSlot, 0, Qt::AlignHCenter);
|
|
||||||
|
|
||||||
m_timeLabel = new QLabel(this);
|
|
||||||
m_timeLabel->setObjectName("seekBubbleTime");
|
|
||||||
m_timeLabel->setAlignment(Qt::AlignCenter);
|
|
||||||
layout->addWidget(m_timeLabel, 0, Qt::AlignHCenter);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SeekTimeBubble::setTimeText(const QString &text)
|
|
||||||
{
|
|
||||||
if (m_timeLabel->text() == text)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_timeLabel->setText(text);
|
|
||||||
adjustSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SeekTimeBubble::showAt(const QPoint &anchor)
|
|
||||||
{
|
|
||||||
adjustSize();
|
|
||||||
|
|
||||||
QPoint topLeft(anchor.x() - width() / 2, anchor.y() - height());
|
|
||||||
if (parentWidget())
|
|
||||||
{
|
|
||||||
const int maxX = parentWidget()->width() - width();
|
|
||||||
topLeft.setX(qBound(0, topLeft.x(), qMax(0, maxX)));
|
|
||||||
topLeft.setY(qMax(0, topLeft.y()));
|
|
||||||
}
|
|
||||||
move(topLeft);
|
|
||||||
|
|
||||||
if (!m_shown)
|
|
||||||
{
|
|
||||||
m_shown = true;
|
|
||||||
raise();
|
|
||||||
FadeHelper::fadeIn(this, AnimationTokens::DurationMs::kQuick);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SeekTimeBubble::hideBubble()
|
|
||||||
{
|
|
||||||
if (!m_shown)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_shown = false;
|
|
||||||
FadeHelper::fadeOut(this, AnimationTokens::DurationMs::kMicro,
|
|
||||||
AnimationTokens::Curve::exit(), [this]() { hide(); });
|
|
||||||
}
|
|
||||||
|
|
||||||
void SeekTimeBubble::paintEvent(QPaintEvent *event)
|
|
||||||
{
|
|
||||||
Q_UNUSED(event);
|
|
||||||
QPainter painter(this);
|
|
||||||
painter.setRenderHint(QPainter::Antialiasing);
|
|
||||||
painter.setPen(QPen(QColor(148, 163, 184, 60), 1));
|
|
||||||
painter.setBrush(QColor(15, 23, 42, 235));
|
|
||||||
painter.drawRoundedRect(QRectF(rect()).adjusted(0.5, 0.5, -0.5, -0.5), 6, 6);
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
#ifndef SEEKTIMEBUBBLE_H
|
|
||||||
#define SEEKTIMEBUBBLE_H
|
|
||||||
|
|
||||||
#include <QWidget>
|
|
||||||
|
|
||||||
class QLabel;
|
|
||||||
|
|
||||||
// Small floating bubble shown above the player seek bar while hovering,
|
|
||||||
// displaying the timestamp under the cursor. Fades in kQuick / out kMicro
|
|
||||||
// (via FadeHelper, so reduce-motion snaps). The layout reserves a hidden
|
|
||||||
// thumbnail slot above the time label so a trickplay preview image can be
|
|
||||||
// added later without re-layouting.
|
|
||||||
//
|
|
||||||
// The bubble is an always-dark player surface, styled by objectName
|
|
||||||
// "seekBubbleTime" in the QSS plus a self-painted panel.
|
|
||||||
class SeekTimeBubble : public QWidget
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit SeekTimeBubble(QWidget *parent);
|
|
||||||
|
|
||||||
void setTimeText(const QString &text);
|
|
||||||
// Anchor = bottom-center of the bubble, in parent coordinates. Clamped to
|
|
||||||
// stay inside the parent. Fades in on the first call after a hide.
|
|
||||||
void showAt(const QPoint &anchor);
|
|
||||||
void hideBubble();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void paintEvent(QPaintEvent *event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
QLabel *m_thumbSlot;
|
|
||||||
QLabel *m_timeLabel;
|
|
||||||
bool m_shown = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // SEEKTIMEBUBBLE_H
|
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
#include "serverwheelview.h"
|
|
||||||
#include <QVariantAnimation>
|
|
||||||
#include <QEasingCurve>
|
|
||||||
#include <QGraphicsOpacityEffect>
|
|
||||||
#include <QMouseEvent>
|
|
||||||
#include <QWheelEvent>
|
|
||||||
#include <QDateTime>
|
|
||||||
#include <QMap>
|
|
||||||
#include <QtMath>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QFont>
|
|
||||||
#include "../tokens/AnimationTokens.h"
|
|
||||||
#include "../utils/motionprefs.h"
|
|
||||||
|
|
||||||
ServerWheelView::ServerWheelView(QWidget* parent) : QWidget(parent) {
|
|
||||||
setAttribute(Qt::WA_StyledBackground, true);
|
|
||||||
setStyleSheet("ServerWheelView { background: transparent; }");
|
|
||||||
|
|
||||||
m_scrollAnim = new QVariantAnimation(this);
|
|
||||||
|
|
||||||
m_scrollAnim->setEasingCurve(AnimationTokens::Curve::enter());
|
|
||||||
m_scrollAnim->setDuration(AnimationTokens::DurationMs::kSlow);
|
|
||||||
connect(m_scrollAnim, &QVariantAnimation::valueChanged, this, [this](const QVariant& val){
|
|
||||||
m_scrollOffset = val.toReal();
|
|
||||||
updateLayout();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ServerWheelView::~ServerWheelView() {
|
|
||||||
clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerWheelView::addCard(QWidget* card) {
|
|
||||||
card->setParent(this);
|
|
||||||
auto* effect = new QGraphicsOpacityEffect(card);
|
|
||||||
card->setGraphicsEffect(effect);
|
|
||||||
|
|
||||||
installFilterRecursively(card);
|
|
||||||
m_items.append(card);
|
|
||||||
|
|
||||||
updateLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerWheelView::clear() {
|
|
||||||
m_scrollAnim->stop();
|
|
||||||
m_scrollOffset = 0.0;
|
|
||||||
m_currentIndex = 0;
|
|
||||||
qDeleteAll(m_items);
|
|
||||||
m_items.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
int ServerWheelView::count() const {
|
|
||||||
return m_items.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerWheelView::setCurrentIndex(int index, bool animated) {
|
|
||||||
int N = m_items.size();
|
|
||||||
if (N == 0) return;
|
|
||||||
|
|
||||||
index = index % N;
|
|
||||||
if (index < 0) index += N;
|
|
||||||
|
|
||||||
if (animated) {
|
|
||||||
qreal delta = index - m_scrollOffset;
|
|
||||||
qreal wrapped_delta = delta - N * qRound(delta / static_cast<qreal>(N));
|
|
||||||
scrollTo(m_scrollOffset + wrapped_delta);
|
|
||||||
} else {
|
|
||||||
m_scrollAnim->stop();
|
|
||||||
m_scrollOffset = index;
|
|
||||||
updateLayout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int ServerWheelView::currentIndex() const {
|
|
||||||
return m_currentIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerWheelView::setTransitionMode(bool active) {
|
|
||||||
m_isTransitioning = active;
|
|
||||||
if (active) {
|
|
||||||
m_scrollAnim->stop();
|
|
||||||
m_scrollOffset = m_currentIndex;
|
|
||||||
}
|
|
||||||
updateLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerWheelView::resizeEvent(QResizeEvent*) {
|
|
||||||
updateLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerWheelView::handleMousePress(QPoint globalPos) {
|
|
||||||
if (m_items.isEmpty()) return;
|
|
||||||
m_scrollAnim->stop();
|
|
||||||
m_isDragging = true;
|
|
||||||
m_dragStarted = false;
|
|
||||||
m_lastPos = globalPos;
|
|
||||||
m_dragVelocity = 0.0;
|
|
||||||
m_lastTime = QDateTime::currentMSecsSinceEpoch();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerWheelView::handleMouseMove(QPoint globalPos) {
|
|
||||||
if (!m_isDragging) return;
|
|
||||||
qreal dy = globalPos.y() - m_lastPos.y();
|
|
||||||
if (!m_dragStarted && qAbs(dy) > 5) m_dragStarted = true;
|
|
||||||
if (m_dragStarted) {
|
|
||||||
qreal deltaOffset = -dy / 80.0;
|
|
||||||
qint64 now = QDateTime::currentMSecsSinceEpoch();
|
|
||||||
qint64 dt = now - m_lastTime;
|
|
||||||
if (dt > 0) m_dragVelocity = deltaOffset / dt * 16.0;
|
|
||||||
|
|
||||||
m_scrollOffset += deltaOffset;
|
|
||||||
|
|
||||||
m_lastPos = globalPos;
|
|
||||||
m_lastTime = now;
|
|
||||||
updateLayout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ServerWheelView::handleMouseRelease() {
|
|
||||||
if (!m_isDragging) return false;
|
|
||||||
m_isDragging = false;
|
|
||||||
if (m_dragStarted) {
|
|
||||||
if (!m_items.isEmpty()) {
|
|
||||||
qreal target = qRound(m_scrollOffset + m_dragVelocity * 10.0);
|
|
||||||
scrollTo(target);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerWheelView::mousePressEvent(QMouseEvent* event) {
|
|
||||||
if (event->button() == Qt::LeftButton) handleMousePress(event->globalPosition().toPoint());
|
|
||||||
QWidget::mousePressEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerWheelView::mouseMoveEvent(QMouseEvent* event) {
|
|
||||||
handleMouseMove(event->globalPosition().toPoint());
|
|
||||||
QWidget::mouseMoveEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerWheelView::mouseReleaseEvent(QMouseEvent* event) {
|
|
||||||
if (event->button() == Qt::LeftButton) handleMouseRelease();
|
|
||||||
QWidget::mouseReleaseEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ServerWheelView::eventFilter(QObject* obj, QEvent* event) {
|
|
||||||
if (event->type() == QEvent::MouseButtonPress) {
|
|
||||||
auto* me = static_cast<QMouseEvent*>(event);
|
|
||||||
if (me->button() == Qt::LeftButton) handleMousePress(me->globalPosition().toPoint());
|
|
||||||
}
|
|
||||||
else if (event->type() == QEvent::MouseMove) {
|
|
||||||
auto* me = static_cast<QMouseEvent*>(event);
|
|
||||||
handleMouseMove(me->globalPosition().toPoint());
|
|
||||||
if (m_dragStarted) return true;
|
|
||||||
}
|
|
||||||
else if (event->type() == QEvent::MouseButtonRelease) {
|
|
||||||
auto* me = static_cast<QMouseEvent*>(event);
|
|
||||||
if (me->button() == Qt::LeftButton) {
|
|
||||||
if (handleMouseRelease()) return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return QWidget::eventFilter(obj, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerWheelView::wheelEvent(QWheelEvent* event) {
|
|
||||||
if (m_items.isEmpty()) return;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
qreal angleDelta = event->angleDelta().y();
|
|
||||||
qreal step = -angleDelta / 120.0;
|
|
||||||
|
|
||||||
|
|
||||||
qreal baseTarget = m_scrollAnim->state() == QAbstractAnimation::Running ?
|
|
||||||
m_scrollAnim->endValue().toReal() : qRound(m_scrollOffset);
|
|
||||||
|
|
||||||
scrollTo(baseTarget + step);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerWheelView::installFilterRecursively(QWidget* w) {
|
|
||||||
w->installEventFilter(this);
|
|
||||||
for (auto* child : w->findChildren<QWidget*>()) {
|
|
||||||
child->installEventFilter(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerWheelView::scrollTo(qreal target) {
|
|
||||||
m_scrollAnim->stop();
|
|
||||||
if (MotionPrefs::reduceMotion()) {
|
|
||||||
m_scrollOffset = target;
|
|
||||||
updateLayout();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_scrollAnim->setStartValue(m_scrollOffset);
|
|
||||||
m_scrollAnim->setEndValue(target);
|
|
||||||
m_scrollAnim->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ServerWheelView::updateLayout() {
|
|
||||||
int N = m_items.size();
|
|
||||||
if (N == 0) return;
|
|
||||||
|
|
||||||
int vh = height();
|
|
||||||
int vw = width();
|
|
||||||
int centerY = vh / 2;
|
|
||||||
int baseItemHeight = 72;
|
|
||||||
|
|
||||||
QMap<qreal, QWidget*> zOrderMap;
|
|
||||||
|
|
||||||
int newIndex = qRound(m_scrollOffset) % N;
|
|
||||||
if (newIndex < 0) newIndex += N;
|
|
||||||
|
|
||||||
if (newIndex != m_currentIndex) {
|
|
||||||
m_currentIndex = newIndex;
|
|
||||||
Q_EMIT currentIndexChanged(m_currentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < N; ++i) {
|
|
||||||
QWidget* card = m_items[i];
|
|
||||||
|
|
||||||
qreal rawDelta = i - m_scrollOffset;
|
|
||||||
qreal logicalIdx = rawDelta - N * qRound(rawDelta / static_cast<qreal>(N));
|
|
||||||
qreal distance = qAbs(logicalIdx);
|
|
||||||
|
|
||||||
qreal opacity = qMax(0.0, 1.0 - distance * 0.4);
|
|
||||||
bool shouldHide = (opacity <= 0.0 || (m_isTransitioning && i != m_currentIndex));
|
|
||||||
|
|
||||||
|
|
||||||
if (shouldHide) {
|
|
||||||
if (card->isVisible()) card->hide();
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
if (!card->isVisible()) card->show();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (card->testAttribute(Qt::WA_TransparentForMouseEvents)) {
|
|
||||||
card->setAttribute(Qt::WA_TransparentForMouseEvents, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isDepthCard = (distance > 1.5);
|
|
||||||
if (card->property("isDepthCard").toBool() != isDepthCard) {
|
|
||||||
card->setProperty("isDepthCard", isDepthCard);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (isDepthCard) {
|
|
||||||
QWidget* actionContainer = nullptr;
|
|
||||||
if (card->property("cachedActionPtr").isValid()) {
|
|
||||||
actionContainer = reinterpret_cast<QWidget*>(card->property("cachedActionPtr").toULongLong());
|
|
||||||
} else {
|
|
||||||
for (auto* btn : card->findChildren<QPushButton*>()) {
|
|
||||||
if (btn->objectName() == "action-edit-btn" && btn->parentWidget()) {
|
|
||||||
actionContainer = btn->parentWidget();
|
|
||||||
card->setProperty("cachedActionPtr", reinterpret_cast<qulonglong>(actionContainer));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (actionContainer && actionContainer->isVisible()) {
|
|
||||||
actionContainer->hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
qreal angle = logicalIdx * (M_PI / 6.0);
|
|
||||||
qreal R = vh / 2.0;
|
|
||||||
qreal scale = qMax(0.5, 1.0 - distance * 0.2);
|
|
||||||
|
|
||||||
int projectedW = vw * scale;
|
|
||||||
int projectedH = baseItemHeight * scale;
|
|
||||||
int projectedX = (vw - projectedW) / 2;
|
|
||||||
int projectedY = centerY + R * qSin(angle) - projectedH / 2.0;
|
|
||||||
|
|
||||||
|
|
||||||
QRect newGeometry(projectedX, projectedY, projectedW, projectedH);
|
|
||||||
if (card->geometry() != newGeometry) {
|
|
||||||
card->setGeometry(newGeometry);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto* child : card->findChildren<QWidget*>()) {
|
|
||||||
|
|
||||||
if (qobject_cast<QLabel*>(child) || qobject_cast<QPushButton*>(child)) {
|
|
||||||
if (!child->property("originalFontSize").isValid()) {
|
|
||||||
QFont f = child->font();
|
|
||||||
qreal size = f.pixelSize() > 0 ? f.pixelSize() : f.pointSizeF();
|
|
||||||
child->setProperty("originalFontSize", size);
|
|
||||||
child->setProperty("usePixel", f.pixelSize() > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
qreal origSize = child->property("originalFontSize").toReal();
|
|
||||||
bool usePixel = child->property("usePixel").toBool();
|
|
||||||
int newSizeInt = qMax(1, qRound(origSize * scale));
|
|
||||||
int lastSizeInt = child->property("lastAppliedFontSize").toInt();
|
|
||||||
|
|
||||||
if (newSizeInt != lastSizeInt) {
|
|
||||||
child->setProperty("lastAppliedFontSize", newSizeInt);
|
|
||||||
|
|
||||||
QFont newFont = child->font();
|
|
||||||
if (usePixel) {
|
|
||||||
newFont.setPixelSize(newSizeInt);
|
|
||||||
} else {
|
|
||||||
newFont.setPointSizeF(newSizeInt);
|
|
||||||
}
|
|
||||||
child->setFont(newFont);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (QLabel* label = qobject_cast<QLabel*>(child)) {
|
|
||||||
if (!label->property("originalIconSize").isValid() &&
|
|
||||||
label->minimumWidth() == label->maximumWidth() &&
|
|
||||||
label->minimumHeight() == label->maximumHeight() &&
|
|
||||||
label->minimumWidth() > 0) {
|
|
||||||
label->setProperty("originalIconSize", label->minimumWidth());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (label->property("originalIconSize").isValid()) {
|
|
||||||
int origIconSize = label->property("originalIconSize").toInt();
|
|
||||||
int newSize = qMax(1, qRound(origIconSize * scale));
|
|
||||||
int lastIconSize = label->property("lastAppliedIconSize").toInt();
|
|
||||||
|
|
||||||
if (newSize != lastIconSize) {
|
|
||||||
label->setProperty("lastAppliedIconSize", newSize);
|
|
||||||
label->setFixedSize(newSize, newSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto* effect = qobject_cast<QGraphicsOpacityEffect*>(card->graphicsEffect())) {
|
|
||||||
|
|
||||||
if (!qFuzzyCompare(effect->opacity(), opacity)) {
|
|
||||||
effect->setOpacity(opacity);
|
|
||||||
}
|
|
||||||
bool enableEffect = opacity < 0.99;
|
|
||||||
if (effect->isEnabled() != enableEffect) {
|
|
||||||
effect->setEnabled(enableEffect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
zOrderMap.insert(-distance, card);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto* card : zOrderMap.values()) {
|
|
||||||
card->raise();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
#ifndef SERVERWHEELVIEW_H
|
|
||||||
#define SERVERWHEELVIEW_H
|
|
||||||
|
|
||||||
#include <QWidget>
|
|
||||||
#include <QList>
|
|
||||||
#include <QPoint>
|
|
||||||
|
|
||||||
class QVariantAnimation;
|
|
||||||
|
|
||||||
class ServerWheelView : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit ServerWheelView(QWidget* parent = nullptr);
|
|
||||||
~ServerWheelView() override;
|
|
||||||
|
|
||||||
void addCard(QWidget* card);
|
|
||||||
void clear();
|
|
||||||
int count() const;
|
|
||||||
|
|
||||||
|
|
||||||
void setCurrentIndex(int index, bool animated = false);
|
|
||||||
int currentIndex() const;
|
|
||||||
|
|
||||||
|
|
||||||
void setTransitionMode(bool active);
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
|
|
||||||
void currentIndexChanged(int index);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void resizeEvent(QResizeEvent* event) override;
|
|
||||||
void wheelEvent(QWheelEvent* event) override;
|
|
||||||
|
|
||||||
void mousePressEvent(QMouseEvent* event) override;
|
|
||||||
void mouseMoveEvent(QMouseEvent* event) override;
|
|
||||||
void mouseReleaseEvent(QMouseEvent* event) override;
|
|
||||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void installFilterRecursively(QWidget* w);
|
|
||||||
void scrollTo(qreal target);
|
|
||||||
void updateLayout();
|
|
||||||
|
|
||||||
void handleMousePress(QPoint globalPos);
|
|
||||||
void handleMouseMove(QPoint globalPos);
|
|
||||||
bool handleMouseRelease();
|
|
||||||
|
|
||||||
QList<QWidget*> m_items;
|
|
||||||
qreal m_scrollOffset = 0.0;
|
|
||||||
int m_currentIndex = 0;
|
|
||||||
QVariantAnimation* m_scrollAnim;
|
|
||||||
|
|
||||||
bool m_isTransitioning = false;
|
|
||||||
bool m_isDragging = false;
|
|
||||||
bool m_dragStarted = false;
|
|
||||||
QPoint m_lastPos;
|
|
||||||
qint64 m_lastTime = 0;
|
|
||||||
qreal m_dragVelocity = 0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
#include "settingscard.h"
|
|
||||||
#include "elidedlabel.h"
|
|
||||||
#include "modernswitch.h"
|
|
||||||
#include "moderntaginput.h"
|
|
||||||
#include "../managers/thememanager.h"
|
|
||||||
#include <QComboBox>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QLineEdit>
|
|
||||||
#include <QSlider>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QSpinBox>
|
|
||||||
|
|
||||||
#include "../../qEmbyCore/config/configstore.h"
|
|
||||||
|
|
||||||
SettingsCard::SettingsCard(const QString &iconSvgPath, const QString &title,
|
|
||||||
const QString &description, QWidget *controlWidget,
|
|
||||||
const QString &configKey, QWidget *parent,
|
|
||||||
const QVariant &defaultValue)
|
|
||||||
: QFrame(parent), m_controlWidget(controlWidget), m_configKey(configKey),
|
|
||||||
m_iconSvgPath(iconSvgPath), m_defaultValue(defaultValue) {
|
|
||||||
|
|
||||||
setObjectName("SettingsCard");
|
|
||||||
|
|
||||||
|
|
||||||
if (m_controlWidget) {
|
|
||||||
m_controlWidget->setParent(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
setupUi(title, description);
|
|
||||||
|
|
||||||
|
|
||||||
if (!m_configKey.isEmpty() && m_controlWidget) {
|
|
||||||
setupDataBinding();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
connect(ThemeManager::instance(), &ThemeManager::themeChanged, this,
|
|
||||||
&SettingsCard::onThemeChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsCard::~SettingsCard() = default;
|
|
||||||
|
|
||||||
void SettingsCard::setupUi(const QString &title, const QString &description) {
|
|
||||||
|
|
||||||
m_mainLayout = new QHBoxLayout(this);
|
|
||||||
m_mainLayout->setContentsMargins(16, 12, 16, 12);
|
|
||||||
m_mainLayout->setSpacing(12);
|
|
||||||
|
|
||||||
|
|
||||||
m_iconLabel = new QLabel(this);
|
|
||||||
m_iconLabel->setFixedSize(16, 16);
|
|
||||||
m_iconLabel->setAlignment(Qt::AlignCenter);
|
|
||||||
m_iconLabel->setObjectName("SettingsCardIcon");
|
|
||||||
updateIcon();
|
|
||||||
|
|
||||||
|
|
||||||
auto *textLayout = new QVBoxLayout();
|
|
||||||
textLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
textLayout->setSpacing(2);
|
|
||||||
textLayout->setAlignment(Qt::AlignVCenter);
|
|
||||||
|
|
||||||
m_titleLabel = new QLabel(title, this);
|
|
||||||
m_titleLabel->setObjectName("SettingsCardTitle");
|
|
||||||
|
|
||||||
m_descLabel = new ElidedLabel(this);
|
|
||||||
m_descLabel->setObjectName("SettingsCardDesc");
|
|
||||||
m_descLabel->setFullText(description);
|
|
||||||
|
|
||||||
textLayout->addWidget(m_titleLabel);
|
|
||||||
if (!description.isEmpty()) {
|
|
||||||
textLayout->addWidget(m_descLabel);
|
|
||||||
} else {
|
|
||||||
m_descLabel->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
m_mainLayout->addWidget(m_iconLabel, 0, Qt::AlignVCenter);
|
|
||||||
m_mainLayout->addLayout(textLayout, 1);
|
|
||||||
|
|
||||||
|
|
||||||
if (m_controlWidget) {
|
|
||||||
if (auto *combo = qobject_cast<QComboBox *>(m_controlWidget)) {
|
|
||||||
combo->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
||||||
} else if (auto *btn = qobject_cast<QPushButton *>(m_controlWidget)) {
|
|
||||||
btn->setObjectName("SettingsCardButton");
|
|
||||||
btn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
||||||
} else if (auto *lineEdit = qobject_cast<QLineEdit *>(m_controlWidget)) {
|
|
||||||
lineEdit->setMinimumWidth(200);
|
|
||||||
lineEdit->setFixedHeight(32);
|
|
||||||
lineEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
|
||||||
} else if (auto *spinBox = qobject_cast<QSpinBox *>(m_controlWidget)) {
|
|
||||||
spinBox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
||||||
} else if (qobject_cast<ModernTagInput *>(m_controlWidget)) {
|
|
||||||
|
|
||||||
m_controlWidget->setMinimumWidth(220);
|
|
||||||
m_controlWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
|
||||||
}
|
|
||||||
m_mainLayout->addWidget(m_controlWidget, 0, Qt::AlignVCenter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void SettingsCard::updateIcon() {
|
|
||||||
if (m_iconSvgPath.isEmpty())
|
|
||||||
return;
|
|
||||||
QIcon icon = ThemeManager::getAdaptiveIcon(m_iconSvgPath);
|
|
||||||
m_iconLabel->setPixmap(icon.pixmap(16, 16));
|
|
||||||
}
|
|
||||||
|
|
||||||
void SettingsCard::onThemeChanged() { updateIcon(); }
|
|
||||||
|
|
||||||
void SettingsCard::setupDataBinding() {
|
|
||||||
auto *store = ConfigStore::instance();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (auto *switchControl = qobject_cast<ModernSwitch *>(m_controlWidget)) {
|
|
||||||
|
|
||||||
switchControl->setChecked(store->get<bool>(m_configKey, m_defaultValue.toBool()));
|
|
||||||
|
|
||||||
|
|
||||||
connect(switchControl, &ModernSwitch::toggled, this,
|
|
||||||
[this, store](bool checked) { store->set(m_configKey, checked); });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
else if (auto *comboControl = qobject_cast<QComboBox *>(m_controlWidget)) {
|
|
||||||
|
|
||||||
QString currentVal = store->get<QString>(m_configKey);
|
|
||||||
if (currentVal.isEmpty() && m_defaultValue.isValid()) {
|
|
||||||
currentVal = m_defaultValue.toString();
|
|
||||||
}
|
|
||||||
int idx = comboControl->findData(currentVal);
|
|
||||||
if (idx >= 0) {
|
|
||||||
comboControl->setCurrentIndex(idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
connect(comboControl, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
|
||||||
this, [this, store, comboControl](int index) {
|
|
||||||
store->set(m_configKey, comboControl->itemData(index));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
else if (auto *lineEdit = qobject_cast<QLineEdit *>(m_controlWidget)) {
|
|
||||||
|
|
||||||
lineEdit->setText(store->get<QString>(m_configKey, m_defaultValue.toString()));
|
|
||||||
|
|
||||||
|
|
||||||
connect(lineEdit, &QLineEdit::editingFinished, this,
|
|
||||||
[this, store, lineEdit]() { store->set(m_configKey, lineEdit->text()); });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
else if (auto *spinBox = qobject_cast<QSpinBox *>(m_controlWidget)) {
|
|
||||||
spinBox->setValue(store->get<int>(m_configKey, m_defaultValue.toInt()));
|
|
||||||
|
|
||||||
connect(spinBox, QOverload<int>::of(&QSpinBox::valueChanged), this,
|
|
||||||
[this, store](int value) { store->set(m_configKey, value); });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
else if (auto *tagInput = qobject_cast<ModernTagInput *>(m_controlWidget)) {
|
|
||||||
|
|
||||||
tagInput->setValue(store->get<QString>(m_configKey, m_defaultValue.toString()));
|
|
||||||
|
|
||||||
|
|
||||||
connect(tagInput, &ModernTagInput::valueChanged, this,
|
|
||||||
[this, store](const QString &val) { store->set(m_configKey, val); });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
connect(store, &ConfigStore::valueChanged, this,
|
|
||||||
&SettingsCard::onConfigValueChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SettingsCard::onConfigValueChanged(const QString &key,
|
|
||||||
const QVariant &newValue) {
|
|
||||||
if (key != m_configKey || !m_controlWidget)
|
|
||||||
return;
|
|
||||||
|
|
||||||
|
|
||||||
m_controlWidget->blockSignals(true);
|
|
||||||
|
|
||||||
if (auto *switchControl = qobject_cast<ModernSwitch *>(m_controlWidget)) {
|
|
||||||
switchControl->setChecked(newValue.toBool());
|
|
||||||
} else if (auto *comboControl = qobject_cast<QComboBox *>(m_controlWidget)) {
|
|
||||||
int idx = comboControl->findData(newValue);
|
|
||||||
if (idx >= 0) {
|
|
||||||
comboControl->setCurrentIndex(idx);
|
|
||||||
}
|
|
||||||
} else if (auto *lineEdit = qobject_cast<QLineEdit *>(m_controlWidget)) {
|
|
||||||
lineEdit->setText(newValue.toString());
|
|
||||||
} else if (auto *spinBox = qobject_cast<QSpinBox *>(m_controlWidget)) {
|
|
||||||
spinBox->setValue(newValue.toInt());
|
|
||||||
} else if (auto *tagInput = qobject_cast<ModernTagInput *>(m_controlWidget)) {
|
|
||||||
tagInput->setValue(newValue.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
m_controlWidget->blockSignals(false);
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
#ifndef SETTINGSCARD_H
|
|
||||||
#define SETTINGSCARD_H
|
|
||||||
|
|
||||||
#include <QFrame>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QIcon>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QString>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
#include <QVariant>
|
|
||||||
#include <QWidget>
|
|
||||||
|
|
||||||
class ElidedLabel;
|
|
||||||
|
|
||||||
class SettingsCard : public QFrame {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
|
|
||||||
explicit SettingsCard(const QString &iconSvgPath, const QString &title,
|
|
||||||
const QString &description, QWidget *controlWidget,
|
|
||||||
const QString &configKey, QWidget *parent = nullptr,
|
|
||||||
const QVariant &defaultValue = QVariant());
|
|
||||||
|
|
||||||
~SettingsCard() override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void setupUi(const QString &title, const QString &description);
|
|
||||||
void setupDataBinding();
|
|
||||||
void updateIcon();
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void onConfigValueChanged(const QString &key, const QVariant &newValue);
|
|
||||||
void onThemeChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QLabel *m_iconLabel;
|
|
||||||
QLabel *m_titleLabel;
|
|
||||||
ElidedLabel *m_descLabel;
|
|
||||||
QWidget *m_controlWidget;
|
|
||||||
QString m_configKey;
|
|
||||||
QString m_iconSvgPath;
|
|
||||||
QVariant m_defaultValue;
|
|
||||||
|
|
||||||
QHBoxLayout *m_mainLayout;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,429 +0,0 @@
|
|||||||
#include "settingssubpanel.h"
|
|
||||||
#include "../managers/thememanager.h"
|
|
||||||
#include "../tokens/AnimationTokens.h"
|
|
||||||
#include <config/configstore.h>
|
|
||||||
#include <config/config_keys.h>
|
|
||||||
#include <QScrollArea>
|
|
||||||
#include <QScrollBar>
|
|
||||||
#include <QTimer>
|
|
||||||
|
|
||||||
SettingsSubPanel::SettingsSubPanel(const QString &iconPath, QWidget *parent)
|
|
||||||
: QFrame(parent), m_iconPath(iconPath) {
|
|
||||||
setObjectName("SettingsSubPanel");
|
|
||||||
|
|
||||||
|
|
||||||
setMaximumHeight(0);
|
|
||||||
setMinimumHeight(0);
|
|
||||||
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
|
|
||||||
|
|
||||||
|
|
||||||
m_outerLayout = new QHBoxLayout(this);
|
|
||||||
m_outerLayout->setContentsMargins(16, 8, 16, 8);
|
|
||||||
m_outerLayout->setSpacing(12);
|
|
||||||
|
|
||||||
|
|
||||||
m_iconLabel = new QLabel(this);
|
|
||||||
m_iconLabel->setFixedSize(16, 16);
|
|
||||||
m_iconLabel->setAlignment(Qt::AlignCenter);
|
|
||||||
m_iconLabel->setObjectName("SettingsCardIcon");
|
|
||||||
updateIcon();
|
|
||||||
|
|
||||||
m_outerLayout->addWidget(m_iconLabel, 0, Qt::AlignVCenter);
|
|
||||||
|
|
||||||
|
|
||||||
m_contentLayout = new QHBoxLayout();
|
|
||||||
m_contentLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
m_contentLayout->setSpacing(12);
|
|
||||||
m_contentLayout->setAlignment(Qt::AlignVCenter);
|
|
||||||
m_outerLayout->addLayout(m_contentLayout, 1);
|
|
||||||
|
|
||||||
|
|
||||||
m_animation = new QPropertyAnimation(this, "panelHeight", this);
|
|
||||||
m_animation->setDuration(AnimationTokens::DurationMs::kNormal);
|
|
||||||
m_animation->setEasingCurve(AnimationTokens::Curve::enter());
|
|
||||||
|
|
||||||
|
|
||||||
connect(m_animation, &QPropertyAnimation::finished, this, [this]() {
|
|
||||||
if (!m_expanded) {
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
connect(ThemeManager::instance(), &ThemeManager::themeChanged,
|
|
||||||
this, [this]() {
|
|
||||||
updateStyleSheet();
|
|
||||||
updateIcon();
|
|
||||||
});
|
|
||||||
updateStyleSheet();
|
|
||||||
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SettingsSubPanel::updateIcon() {
|
|
||||||
if (m_iconPath.isEmpty()) return;
|
|
||||||
QIcon icon = ThemeManager::getAdaptiveIcon(m_iconPath);
|
|
||||||
m_iconLabel->setPixmap(icon.pixmap(16, 16));
|
|
||||||
}
|
|
||||||
|
|
||||||
void SettingsSubPanel::updateStyleSheet() {
|
|
||||||
bool isDark = ThemeManager::instance()->isDarkMode();
|
|
||||||
|
|
||||||
|
|
||||||
QString accent = isDark ? "#3B82F6" : "#4A90D9";
|
|
||||||
|
|
||||||
|
|
||||||
if (isDark) {
|
|
||||||
setStyleSheet(QString(
|
|
||||||
|
|
||||||
"#SettingsSubPanel {"
|
|
||||||
" background: rgba(255,255,255,0.025);"
|
|
||||||
" border: 1px solid rgba(255,255,255,0.06);"
|
|
||||||
" border-left: 3px solid %1;"
|
|
||||||
" border-radius: 12px;"
|
|
||||||
" margin-left: 20px;"
|
|
||||||
"}"
|
|
||||||
|
|
||||||
"#SettingsSubPanel ModernComboBox {"
|
|
||||||
" background-color: rgba(255,255,255,0.07);"
|
|
||||||
" border: 1px solid rgba(255,255,255,0.10);"
|
|
||||||
" border-radius: 8px;"
|
|
||||||
" color: #E2E8F0;"
|
|
||||||
" font-size: 13px; font-weight: 500;"
|
|
||||||
" font-family: 'Segoe UI','Microsoft YaHei',sans-serif;"
|
|
||||||
" padding: 5px 32px 5px 16px;"
|
|
||||||
" min-width: 120px;"
|
|
||||||
" selection-background-color: transparent;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel ModernComboBox:hover {"
|
|
||||||
" background-color: rgba(255,255,255,0.10);"
|
|
||||||
" border-color: rgba(255,255,255,0.18);"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel ModernComboBox:focus {"
|
|
||||||
" border-color: #3B82F6;"
|
|
||||||
" background-color: rgba(59,130,246,0.08);"
|
|
||||||
" outline: none;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel ModernComboBox::drop-down {"
|
|
||||||
" subcontrol-origin: padding; subcontrol-position: center right;"
|
|
||||||
" width: 32px; border: none; background: transparent;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel ModernComboBox::down-arrow {"
|
|
||||||
" image: url(':/svg/dark/chevron-down.svg'); width: 14px; height: 14px;"
|
|
||||||
"}"
|
|
||||||
|
|
||||||
"#SettingsSubPanel ModernComboBox QAbstractItemView {"
|
|
||||||
" background-color: #1E293B;"
|
|
||||||
" border: 1px solid rgba(255,255,255,0.10);"
|
|
||||||
" border-radius: 8px; color: #E2E8F0; font-size: 13px;"
|
|
||||||
" outline: none; padding: 4px;"
|
|
||||||
" selection-background-color: rgba(59,130,246,0.20);"
|
|
||||||
" selection-color: #93C5FD;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel ModernComboBox QAbstractItemView::item {"
|
|
||||||
" padding: 5px 12px; border-radius: 6px;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel ModernComboBox QAbstractItemView::item:hover {"
|
|
||||||
" background-color: rgba(255,255,255,0.07); color: #F8FAFC;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel ModernComboBox QAbstractItemView::item:selected {"
|
|
||||||
" background-color: rgba(59,130,246,0.20); color: #93C5FD;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel ModernComboBox QAbstractItemView::item:selected:hover {"
|
|
||||||
" background-color: rgba(59,130,246,0.20); color: #93C5FD;"
|
|
||||||
"}"
|
|
||||||
|
|
||||||
"#SettingsSubPanel #SettingsCardDesc {"
|
|
||||||
" color: #64748B;"
|
|
||||||
" font-size: 12px;"
|
|
||||||
" font-family: 'Segoe UI','Microsoft YaHei',sans-serif;"
|
|
||||||
"}"
|
|
||||||
|
|
||||||
"#SettingsSubPanel QLineEdit {"
|
|
||||||
" background-color: rgba(255,255,255,0.07);"
|
|
||||||
" border: 1px solid rgba(255,255,255,0.10);"
|
|
||||||
" border-radius: 8px;"
|
|
||||||
" color: #E2E8F0;"
|
|
||||||
" font-size: 13px; font-weight: 500;"
|
|
||||||
" font-family: 'Segoe UI','Microsoft YaHei',sans-serif;"
|
|
||||||
" padding: 5px 12px;"
|
|
||||||
" max-width: 280px;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel QLineEdit:hover {"
|
|
||||||
" border-color: rgba(255,255,255,0.18);"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel QLineEdit:focus {"
|
|
||||||
" border-color: #3B82F6;"
|
|
||||||
" background-color: rgba(59,130,246,0.08);"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel QLineEdit#DanmakuOptionSliderValue,"
|
|
||||||
"#SettingsSubPanel QLineEdit#DanmakuOptionSliderValue:hover,"
|
|
||||||
"#SettingsSubPanel QLineEdit#DanmakuOptionSliderValue:focus,"
|
|
||||||
"#SettingsSubPanel QLineEdit#SubtitleOptionSliderValue,"
|
|
||||||
"#SettingsSubPanel QLineEdit#SubtitleOptionSliderValue:hover,"
|
|
||||||
"#SettingsSubPanel QLineEdit#SubtitleOptionSliderValue:focus {"
|
|
||||||
" background: transparent;"
|
|
||||||
" border: none;"
|
|
||||||
" border-radius: 0px;"
|
|
||||||
" color: #BFDBFE;"
|
|
||||||
" font-size: 13px;"
|
|
||||||
" font-weight: 700;"
|
|
||||||
" font-family: 'Segoe UI','Microsoft YaHei',sans-serif;"
|
|
||||||
" padding: 0px;"
|
|
||||||
" min-width: 0px;"
|
|
||||||
" max-width: 16777215px;"
|
|
||||||
" selection-background-color: rgba(96,165,250,0.22);"
|
|
||||||
" selection-color: #F8FAFC;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel QFrame#TagInputContainer {"
|
|
||||||
" background-color: rgba(255,255,255,0.07);"
|
|
||||||
" border: 1px solid rgba(255,255,255,0.10);"
|
|
||||||
" border-radius: 8px;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel QFrame#TagInputContainer:hover {"
|
|
||||||
" border-color: rgba(255,255,255,0.18);"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel QFrame#TagInputContainer:focus-within {"
|
|
||||||
" border-color: #3B82F6;"
|
|
||||||
" background-color: rgba(255,255,255,0.07);"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel QLineEdit#TagInputInlineEdit,"
|
|
||||||
"#SettingsSubPanel QLineEdit#TagInputInlineEdit:hover,"
|
|
||||||
"#SettingsSubPanel QLineEdit#TagInputInlineEdit:focus {"
|
|
||||||
" background: transparent;"
|
|
||||||
" border: none;"
|
|
||||||
" border-radius: 0px;"
|
|
||||||
" color: #E2E8F0;"
|
|
||||||
" font-size: 13px;"
|
|
||||||
" font-weight: 500;"
|
|
||||||
" font-family: 'Segoe UI','Microsoft YaHei',sans-serif;"
|
|
||||||
" padding: 0px 4px;"
|
|
||||||
" min-width: 0px;"
|
|
||||||
" max-width: 16777215px;"
|
|
||||||
" selection-background-color: rgba(59,130,246,0.25);"
|
|
||||||
" selection-color: #FFFFFF;"
|
|
||||||
"}"
|
|
||||||
).arg(accent));
|
|
||||||
} else {
|
|
||||||
setStyleSheet(QString(
|
|
||||||
|
|
||||||
"#SettingsSubPanel {"
|
|
||||||
" background: rgba(0,0,0,0.018);"
|
|
||||||
" border: 1px solid rgba(0,0,0,0.06);"
|
|
||||||
" border-left: 3px solid %1;"
|
|
||||||
" border-radius: 12px;"
|
|
||||||
" margin-left: 20px;"
|
|
||||||
"}"
|
|
||||||
|
|
||||||
"#SettingsSubPanel ModernComboBox {"
|
|
||||||
" background-color: #FFFFFF;"
|
|
||||||
" border: 1px solid #CBD5E1;"
|
|
||||||
" border-radius: 8px;"
|
|
||||||
" color: #1E293B;"
|
|
||||||
" font-size: 13px; font-weight: 500;"
|
|
||||||
" font-family: 'Segoe UI','Microsoft YaHei',sans-serif;"
|
|
||||||
" padding: 5px 32px 5px 16px;"
|
|
||||||
" min-width: 120px;"
|
|
||||||
" selection-background-color: transparent;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel ModernComboBox:hover {"
|
|
||||||
" background-color: #F8FAFC; border-color: #94A3B8;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel ModernComboBox:focus {"
|
|
||||||
" border-color: #3B82F6; background-color: #EFF6FF; outline: none;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel ModernComboBox::drop-down {"
|
|
||||||
" subcontrol-origin: padding; subcontrol-position: center right;"
|
|
||||||
" width: 32px; border: none; background: transparent;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel ModernComboBox::down-arrow {"
|
|
||||||
" image: url(':/svg/light/chevron-down.svg'); width: 14px; height: 14px;"
|
|
||||||
"}"
|
|
||||||
|
|
||||||
"#SettingsSubPanel ModernComboBox QAbstractItemView {"
|
|
||||||
" background-color: #FFFFFF;"
|
|
||||||
" border: 1px solid #E2E8F0;"
|
|
||||||
" border-radius: 8px; color: #1E293B; font-size: 13px;"
|
|
||||||
" outline: none; padding: 4px;"
|
|
||||||
" selection-background-color: rgba(59,130,246,0.10);"
|
|
||||||
" selection-color: #2563EB;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel ModernComboBox QAbstractItemView::item {"
|
|
||||||
" padding: 5px 12px; border-radius: 6px;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel ModernComboBox QAbstractItemView::item:hover {"
|
|
||||||
" background-color: #F1F5F9; color: #0F172A;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel ModernComboBox QAbstractItemView::item:selected {"
|
|
||||||
" background-color: rgba(59,130,246,0.10); color: #2563EB;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel ModernComboBox QAbstractItemView::item:selected:hover {"
|
|
||||||
" background-color: rgba(59,130,246,0.10); color: #2563EB;"
|
|
||||||
"}"
|
|
||||||
|
|
||||||
"#SettingsSubPanel #SettingsCardDesc {"
|
|
||||||
" color: #94A3B8;"
|
|
||||||
" font-size: 12px;"
|
|
||||||
" font-family: 'Segoe UI','Microsoft YaHei',sans-serif;"
|
|
||||||
"}"
|
|
||||||
|
|
||||||
"#SettingsSubPanel QLineEdit {"
|
|
||||||
" background-color: #FFFFFF;"
|
|
||||||
" border: 1px solid #CBD5E1;"
|
|
||||||
" border-radius: 8px;"
|
|
||||||
" color: #1E293B;"
|
|
||||||
" font-size: 13px; font-weight: 500;"
|
|
||||||
" font-family: 'Segoe UI','Microsoft YaHei',sans-serif;"
|
|
||||||
" padding: 5px 12px;"
|
|
||||||
" max-width: 280px;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel QLineEdit:hover {"
|
|
||||||
" border-color: #94A3B8;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel QLineEdit:focus {"
|
|
||||||
" border-color: #3B82F6; background-color: #EFF6FF;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel QLineEdit#DanmakuOptionSliderValue,"
|
|
||||||
"#SettingsSubPanel QLineEdit#DanmakuOptionSliderValue:hover,"
|
|
||||||
"#SettingsSubPanel QLineEdit#DanmakuOptionSliderValue:focus,"
|
|
||||||
"#SettingsSubPanel QLineEdit#SubtitleOptionSliderValue,"
|
|
||||||
"#SettingsSubPanel QLineEdit#SubtitleOptionSliderValue:hover,"
|
|
||||||
"#SettingsSubPanel QLineEdit#SubtitleOptionSliderValue:focus {"
|
|
||||||
" background: transparent;"
|
|
||||||
" border: none;"
|
|
||||||
" border-radius: 0px;"
|
|
||||||
" color: #2563EB;"
|
|
||||||
" font-size: 13px;"
|
|
||||||
" font-weight: 700;"
|
|
||||||
" font-family: 'Segoe UI','Microsoft YaHei',sans-serif;"
|
|
||||||
" padding: 0px;"
|
|
||||||
" min-width: 0px;"
|
|
||||||
" max-width: 16777215px;"
|
|
||||||
" selection-background-color: rgba(59,130,246,0.16);"
|
|
||||||
" selection-color: #0F172A;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel QFrame#TagInputContainer {"
|
|
||||||
" background-color: #FFFFFF;"
|
|
||||||
" border: 1px solid #CBD5E1;"
|
|
||||||
" border-radius: 8px;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel QFrame#TagInputContainer:hover {"
|
|
||||||
" border-color: #94A3B8;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel QFrame#TagInputContainer:focus-within {"
|
|
||||||
" border-color: #3B82F6;"
|
|
||||||
" background-color: #FFFFFF;"
|
|
||||||
"}"
|
|
||||||
"#SettingsSubPanel QLineEdit#TagInputInlineEdit,"
|
|
||||||
"#SettingsSubPanel QLineEdit#TagInputInlineEdit:hover,"
|
|
||||||
"#SettingsSubPanel QLineEdit#TagInputInlineEdit:focus {"
|
|
||||||
" background: transparent;"
|
|
||||||
" border: none;"
|
|
||||||
" border-radius: 0px;"
|
|
||||||
" color: #1E293B;"
|
|
||||||
" font-size: 13px;"
|
|
||||||
" font-weight: 500;"
|
|
||||||
" font-family: 'Segoe UI','Microsoft YaHei',sans-serif;"
|
|
||||||
" padding: 0px 4px;"
|
|
||||||
" min-width: 0px;"
|
|
||||||
" max-width: 16777215px;"
|
|
||||||
" selection-background-color: rgba(59,130,246,0.25);"
|
|
||||||
" selection-color: #1E293B;"
|
|
||||||
"}"
|
|
||||||
).arg(accent));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SettingsSubPanel::expand() {
|
|
||||||
if (m_expanded) return;
|
|
||||||
m_expanded = true;
|
|
||||||
show();
|
|
||||||
|
|
||||||
|
|
||||||
auto scrollToSelf = [this]() {
|
|
||||||
QWidget *p = parentWidget();
|
|
||||||
while (p) {
|
|
||||||
if (auto *scrollArea = qobject_cast<QScrollArea *>(p)) {
|
|
||||||
scrollArea->ensureWidgetVisible(this, 0, 60);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
p = p->parentWidget();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
int targetH = contentHeight();
|
|
||||||
bool reduceAnimations = ConfigStore::instance()->get<bool>(ConfigKeys::UiAnimations, false);
|
|
||||||
if (reduceAnimations) {
|
|
||||||
setPanelHeight(targetH);
|
|
||||||
|
|
||||||
QTimer::singleShot(0, this, scrollToSelf);
|
|
||||||
} else {
|
|
||||||
m_animation->stop();
|
|
||||||
m_animation->setStartValue(0);
|
|
||||||
m_animation->setEndValue(targetH);
|
|
||||||
m_animation->start();
|
|
||||||
|
|
||||||
connect(m_animation, &QPropertyAnimation::finished,
|
|
||||||
this, scrollToSelf, Qt::SingleShotConnection);
|
|
||||||
}
|
|
||||||
emit expandedChanged(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SettingsSubPanel::collapse() {
|
|
||||||
if (!m_expanded) return;
|
|
||||||
m_expanded = false;
|
|
||||||
|
|
||||||
bool reduceAnimations = ConfigStore::instance()->get<bool>(ConfigKeys::UiAnimations, false);
|
|
||||||
if (reduceAnimations) {
|
|
||||||
setPanelHeight(0);
|
|
||||||
hide();
|
|
||||||
} else {
|
|
||||||
m_animation->stop();
|
|
||||||
m_animation->setStartValue(maximumHeight());
|
|
||||||
m_animation->setEndValue(0);
|
|
||||||
m_animation->start();
|
|
||||||
}
|
|
||||||
emit expandedChanged(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SettingsSubPanel::setExpanded(bool expanded) {
|
|
||||||
if (expanded) expand();
|
|
||||||
else collapse();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SettingsSubPanel::setExpandedImmediately(bool expanded) {
|
|
||||||
m_animation->stop();
|
|
||||||
m_expanded = expanded;
|
|
||||||
if (expanded) {
|
|
||||||
setPanelHeight(contentHeight());
|
|
||||||
show();
|
|
||||||
} else {
|
|
||||||
setPanelHeight(0);
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
emit expandedChanged(expanded);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SettingsSubPanel::initExpanded() {
|
|
||||||
|
|
||||||
|
|
||||||
setExpandedImmediately(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
int SettingsSubPanel::panelHeight() const {
|
|
||||||
return maximumHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SettingsSubPanel::setPanelHeight(int h) {
|
|
||||||
setMaximumHeight(h);
|
|
||||||
setMinimumHeight(h);
|
|
||||||
}
|
|
||||||
|
|
||||||
int SettingsSubPanel::contentHeight() const {
|
|
||||||
if (!m_outerLayout) {
|
|
||||||
return 48;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_outerLayout->invalidate();
|
|
||||||
return qMax(48, m_outerLayout->sizeHint().height());
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
#ifndef SETTINGSSUBPANEL_H
|
|
||||||
#define SETTINGSSUBPANEL_H
|
|
||||||
|
|
||||||
#include <QFrame>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QPropertyAnimation>
|
|
||||||
|
|
||||||
|
|
||||||
class SettingsSubPanel : public QFrame {
|
|
||||||
Q_OBJECT
|
|
||||||
Q_PROPERTY(int panelHeight READ panelHeight WRITE setPanelHeight)
|
|
||||||
|
|
||||||
public:
|
|
||||||
|
|
||||||
explicit SettingsSubPanel(const QString &iconPath, QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
|
|
||||||
QHBoxLayout *contentLayout() const { return m_contentLayout; }
|
|
||||||
|
|
||||||
|
|
||||||
bool isExpanded() const { return m_expanded; }
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
void expand();
|
|
||||||
void collapse();
|
|
||||||
void setExpanded(bool expanded);
|
|
||||||
void setExpandedImmediately(bool expanded);
|
|
||||||
|
|
||||||
void initExpanded();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void expandedChanged(bool expanded);
|
|
||||||
|
|
||||||
private:
|
|
||||||
int panelHeight() const;
|
|
||||||
void setPanelHeight(int h);
|
|
||||||
int contentHeight() const;
|
|
||||||
void updateStyleSheet();
|
|
||||||
void updateIcon();
|
|
||||||
|
|
||||||
bool m_expanded = false;
|
|
||||||
QString m_iconPath;
|
|
||||||
QLabel *m_iconLabel;
|
|
||||||
QHBoxLayout *m_outerLayout;
|
|
||||||
QHBoxLayout *m_contentLayout;
|
|
||||||
QPropertyAnimation *m_animation;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
#include "shadowoverlay.h"
|
|
||||||
|
|
||||||
#include <QPainter>
|
|
||||||
|
|
||||||
ShadowOverlay::ShadowOverlay(QWidget *parent) : QWidget(parent)
|
|
||||||
{
|
|
||||||
setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
|
||||||
setAttribute(Qt::WA_NoSystemBackground, true);
|
|
||||||
setFocusPolicy(Qt::NoFocus);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShadowOverlay::setShadow(int blurRadius, int cornerRadius, const QColor &color)
|
|
||||||
{
|
|
||||||
m_blurRadius = blurRadius;
|
|
||||||
m_cornerRadius = cornerRadius;
|
|
||||||
m_color = color;
|
|
||||||
m_patch = NinePatchShadow{}; // force rebuild on next paint
|
|
||||||
m_builtDpr = 0.0;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShadowOverlay::setBodyCutout(bool on)
|
|
||||||
{
|
|
||||||
if (m_bodyCutout == on)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_bodyCutout = on;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShadowOverlay::ensurePixmap(qreal dpr)
|
|
||||||
{
|
|
||||||
if (!m_patch.isNull() && qFuzzyCompare(m_builtDpr, dpr))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_patch = ShadowPixmap::make(m_blurRadius, m_cornerRadius, m_color, dpr);
|
|
||||||
m_builtDpr = dpr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShadowOverlay::paintEvent(QPaintEvent *)
|
|
||||||
{
|
|
||||||
const qreal dpr = devicePixelRatioF();
|
|
||||||
ensurePixmap(dpr);
|
|
||||||
if (m_patch.isNull())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int b = m_patch.border;
|
|
||||||
const int t = m_patch.total;
|
|
||||||
const qreal s = m_patch.dpr;
|
|
||||||
const QRect r = rect();
|
|
||||||
|
|
||||||
// Degenerate target (smaller than the two fixed borders): nothing sensible
|
|
||||||
// to slice, skip rather than draw garbage.
|
|
||||||
if (r.width() < 2 * b || r.height() < 2 * b)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3x3 grid: corners fixed, edges/centre stretched. Target columns/rows are
|
|
||||||
// logical; source columns/rows are physical (pixmap carries no dpr).
|
|
||||||
const int xt[4] = {r.left(), r.left() + b, r.right() + 1 - b, r.right() + 1};
|
|
||||||
const int yt[4] = {r.top(), r.top() + b, r.bottom() + 1 - b, r.bottom() + 1};
|
|
||||||
const qreal xs[4] = {0.0, b * s, (t - b) * s, t * s};
|
|
||||||
const qreal ys[4] = {0.0, b * s, (t - b) * s, t * s};
|
|
||||||
|
|
||||||
QPainter p(this);
|
|
||||||
if (m_bodyCutout)
|
|
||||||
{
|
|
||||||
// Paint only the ring outside the target's footprint.
|
|
||||||
const int reachPx = reach();
|
|
||||||
const QRect body = r.adjusted(reachPx, reachPx, -reachPx, -reachPx);
|
|
||||||
if (body.isValid())
|
|
||||||
{
|
|
||||||
p.setClipRegion(QRegion(r).subtracted(QRegion(body)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (int i = 0; i < 3; ++i)
|
|
||||||
{
|
|
||||||
for (int j = 0; j < 3; ++j)
|
|
||||||
{
|
|
||||||
const QRectF target(xt[i], yt[j], xt[i + 1] - xt[i], yt[j + 1] - yt[j]);
|
|
||||||
const QRectF source(xs[i], ys[j], xs[i + 1] - xs[i], ys[j + 1] - ys[j]);
|
|
||||||
p.drawPixmap(target, m_patch.pixmap, source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
#ifndef SHADOWOVERLAY_H
|
|
||||||
#define SHADOWOVERLAY_H
|
|
||||||
|
|
||||||
#include <QColor>
|
|
||||||
#include <QWidget>
|
|
||||||
|
|
||||||
#include "../utils/shadowpixmap.h"
|
|
||||||
|
|
||||||
// A mouse-transparent sibling widget that paints a pre-baked drop shadow as a
|
|
||||||
// 9-slice stretch. Use it in place of QGraphicsDropShadowEffect on widgets that
|
|
||||||
// animate: position the overlay just under the target (geometry expanded by
|
|
||||||
// reach() on every side) and it follows with no per-frame blur cost.
|
|
||||||
//
|
|
||||||
// The baked pixmap is built lazily on first paint and rebuilt only when the
|
|
||||||
// device pixel ratio changes, so HiDPI / monitor moves stay crisp.
|
|
||||||
class ShadowOverlay : public QWidget
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit ShadowOverlay(QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
// Configure the shadow. blurRadius/cornerRadius are logical pixels; color
|
|
||||||
// carries the tint and its alpha sets strength. Invalidates any baked pixmap.
|
|
||||||
void setShadow(int blurRadius, int cornerRadius, const QColor &color);
|
|
||||||
|
|
||||||
// How far the shadow extends beyond the target on each side. Callers expand
|
|
||||||
// the target geometry by this amount to get the overlay geometry.
|
|
||||||
int reach() const { return m_blurRadius + 1; }
|
|
||||||
|
|
||||||
// When enabled, the target's footprint (the overlay rect inset by reach()) is
|
|
||||||
// clipped out, so the shadow is painted only as a ring around the target.
|
|
||||||
// Required for semi-transparent targets, where the shadow would otherwise
|
|
||||||
// bleed through the body and darken it.
|
|
||||||
void setBodyCutout(bool on);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void paintEvent(QPaintEvent *event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void ensurePixmap(qreal dpr);
|
|
||||||
|
|
||||||
int m_blurRadius = 0;
|
|
||||||
int m_cornerRadius = 0;
|
|
||||||
QColor m_color;
|
|
||||||
bool m_bodyCutout = false;
|
|
||||||
|
|
||||||
NinePatchShadow m_patch;
|
|
||||||
qreal m_builtDpr = 0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // SHADOWOVERLAY_H
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
#include "shimmerwidget.h"
|
|
||||||
#include "../managers/thememanager.h"
|
|
||||||
#include "../tokens/AnimationTokens.h"
|
|
||||||
#include "../utils/motionprefs.h"
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QPainterPath>
|
|
||||||
#include <QPropertyAnimation>
|
|
||||||
|
|
||||||
ShimmerWidget::ShimmerWidget(QWidget* parent)
|
|
||||||
: QWidget(parent)
|
|
||||||
{
|
|
||||||
setObjectName("shimmer-widget");
|
|
||||||
|
|
||||||
setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
|
||||||
|
|
||||||
m_animation = new QPropertyAnimation(this, "shimmerPhase", this);
|
|
||||||
m_animation->setStartValue(0.0);
|
|
||||||
m_animation->setEndValue(1.0);
|
|
||||||
m_animation->setDuration(AnimationTokens::DurationMs::kShimmer);
|
|
||||||
m_animation->setLoopCount(-1);
|
|
||||||
m_animation->setEasingCurve(AnimationTokens::Curve::loop());
|
|
||||||
}
|
|
||||||
|
|
||||||
ShimmerWidget::~ShimmerWidget()
|
|
||||||
{
|
|
||||||
if (m_animation) {
|
|
||||||
m_animation->stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShimmerWidget::setCardSize(const QSize& size)
|
|
||||||
{
|
|
||||||
m_cardSize = size;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShimmerWidget::setCardSpacing(int spacing)
|
|
||||||
{
|
|
||||||
m_cardSpacing = spacing;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShimmerWidget::setCardRadius(int radius)
|
|
||||||
{
|
|
||||||
m_cardRadius = radius;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShimmerWidget::setShowSubtitle(bool show)
|
|
||||||
{
|
|
||||||
m_showSubtitle = show;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
qreal ShimmerWidget::shimmerPhase() const
|
|
||||||
{
|
|
||||||
return m_shimmerPhase;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShimmerWidget::setShimmerPhase(qreal phase)
|
|
||||||
{
|
|
||||||
m_shimmerPhase = phase;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShimmerWidget::startAnimation()
|
|
||||||
{
|
|
||||||
if (!m_animation || m_animation->state() == QAbstractAnimation::Running) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_shimmerPhase = 0.0;
|
|
||||||
if (MotionPrefs::reduceMotion()) {
|
|
||||||
// Reduce-motion: show a static skeleton (base shapes) with no moving sweep.
|
|
||||||
update();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_animation->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShimmerWidget::stopAnimation()
|
|
||||||
{
|
|
||||||
if (m_animation) {
|
|
||||||
m_animation->stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShimmerWidget::isAnimating() const
|
|
||||||
{
|
|
||||||
return m_animation &&
|
|
||||||
m_animation->state() == QAbstractAnimation::Running;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShimmerWidget::paintEvent(QPaintEvent* event)
|
|
||||||
{
|
|
||||||
Q_UNUSED(event);
|
|
||||||
|
|
||||||
QPainter painter(this);
|
|
||||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
|
||||||
|
|
||||||
const bool isDark = ThemeManager::instance()->isDarkMode();
|
|
||||||
|
|
||||||
|
|
||||||
const QColor baseBg = isDark ? QColor(50, 50, 55) : QColor(229, 231, 235);
|
|
||||||
|
|
||||||
const int padding = 8;
|
|
||||||
const int cardW = m_cardSize.width();
|
|
||||||
const int cardH = m_cardSize.height();
|
|
||||||
const int imgW = cardW - padding * 2;
|
|
||||||
|
|
||||||
|
|
||||||
const qreal ratio =
|
|
||||||
static_cast<qreal>(cardH) / cardW;
|
|
||||||
const int imgH =
|
|
||||||
(ratio < 1.0) ? qRound(imgW * 9.0 / 16.0) : qRound(imgW * 1.5);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
QPainterPath allShapes;
|
|
||||||
const int maxCards = 60;
|
|
||||||
int count = 0;
|
|
||||||
int y = 0;
|
|
||||||
|
|
||||||
while (y < height() + cardH && count < maxCards) {
|
|
||||||
int x = 0;
|
|
||||||
while (x < width() + cardW && count < maxCards) {
|
|
||||||
QRectF imgRect(x + padding, y + padding, imgW, imgH);
|
|
||||||
allShapes.addRoundedRect(imgRect, m_cardRadius, m_cardRadius);
|
|
||||||
|
|
||||||
x += cardW + m_cardSpacing;
|
|
||||||
++count;
|
|
||||||
}
|
|
||||||
y += cardH + m_cardSpacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
painter.setPen(Qt::NoPen);
|
|
||||||
painter.setBrush(baseBg);
|
|
||||||
painter.drawPath(allShapes);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const int totalW = width();
|
|
||||||
const int shimmerW = qRound(totalW * 0.4);
|
|
||||||
const int shimmerX =
|
|
||||||
qRound(m_shimmerPhase * (totalW + shimmerW)) - shimmerW;
|
|
||||||
|
|
||||||
QLinearGradient gradient(shimmerX, 0, shimmerX + shimmerW, 0);
|
|
||||||
const QColor shimmerHi =
|
|
||||||
isDark ? QColor(255, 255, 255, 14) : QColor(255, 255, 255, 100);
|
|
||||||
gradient.setColorAt(0.0, Qt::transparent);
|
|
||||||
gradient.setColorAt(0.5, shimmerHi);
|
|
||||||
gradient.setColorAt(1.0, Qt::transparent);
|
|
||||||
|
|
||||||
painter.save();
|
|
||||||
painter.setClipPath(allShapes);
|
|
||||||
painter.setBrush(gradient);
|
|
||||||
painter.drawRect(rect());
|
|
||||||
painter.restore();
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
#ifndef SHIMMERWIDGET_H
|
|
||||||
#define SHIMMERWIDGET_H
|
|
||||||
|
|
||||||
#include <QWidget>
|
|
||||||
#include <QSize>
|
|
||||||
|
|
||||||
class QPropertyAnimation;
|
|
||||||
|
|
||||||
|
|
||||||
class ShimmerWidget : public QWidget
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
Q_PROPERTY(qreal shimmerPhase READ shimmerPhase WRITE setShimmerPhase)
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit ShimmerWidget(QWidget* parent = nullptr);
|
|
||||||
~ShimmerWidget() override;
|
|
||||||
|
|
||||||
|
|
||||||
void setCardSize(const QSize& size);
|
|
||||||
|
|
||||||
|
|
||||||
void setCardSpacing(int spacing);
|
|
||||||
|
|
||||||
|
|
||||||
void setCardRadius(int radius);
|
|
||||||
|
|
||||||
|
|
||||||
void setShowSubtitle(bool show);
|
|
||||||
|
|
||||||
|
|
||||||
qreal shimmerPhase() const;
|
|
||||||
void setShimmerPhase(qreal phase);
|
|
||||||
|
|
||||||
|
|
||||||
void startAnimation();
|
|
||||||
void stopAnimation();
|
|
||||||
bool isAnimating() const;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void paintEvent(QPaintEvent* event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
QSize m_cardSize{160, 270};
|
|
||||||
int m_cardSpacing = 0;
|
|
||||||
int m_cardRadius = 8;
|
|
||||||
bool m_showSubtitle = true;
|
|
||||||
qreal m_shimmerPhase = 0.0;
|
|
||||||
QPropertyAnimation* m_animation = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,278 +0,0 @@
|
|||||||
#include "splitplayerbutton.h"
|
|
||||||
#include "../managers/externalplayerdetector.h"
|
|
||||||
#include "../utils/popupfade.h"
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QPainterPath>
|
|
||||||
#include <QMouseEvent>
|
|
||||||
#include <QFileIconProvider>
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QWidgetAction>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
#include <QStyle>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SplitPlayerButton::SplitPlayerButton(QWidget *parent)
|
|
||||||
: QWidget(parent)
|
|
||||||
{
|
|
||||||
setMouseTracking(true);
|
|
||||||
setCursor(Qt::PointingHandCursor);
|
|
||||||
setFixedHeight(36);
|
|
||||||
|
|
||||||
m_menu = new QMenu(this);
|
|
||||||
m_menu->setObjectName("immersive-stream-menu");
|
|
||||||
|
|
||||||
m_menu->setWindowFlags(Qt::Popup | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint);
|
|
||||||
m_menu->setAttribute(Qt::WA_TranslucentBackground, true);
|
|
||||||
m_menu->setAutoFillBackground(false);
|
|
||||||
PopupFade::attach(m_menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SplitPlayerButton::setPlayers(const QList<DetectedPlayer> &players,
|
|
||||||
const QString &activePlayerPath)
|
|
||||||
{
|
|
||||||
m_menu->clear();
|
|
||||||
|
|
||||||
if (players.isEmpty()) {
|
|
||||||
m_activeIcon = QIcon();
|
|
||||||
m_activeText.clear();
|
|
||||||
m_activePath.clear();
|
|
||||||
m_hasMultiple = false;
|
|
||||||
hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFileIconProvider iconProvider;
|
|
||||||
m_activePath = activePlayerPath;
|
|
||||||
m_hasMultiple = (players.size() > 1);
|
|
||||||
|
|
||||||
|
|
||||||
int activeIdx = 0;
|
|
||||||
for (int i = 0; i < players.size(); ++i) {
|
|
||||||
if (players[i].path == activePlayerPath) {
|
|
||||||
activeIdx = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_activeIcon = iconProvider.icon(QFileInfo(players[activeIdx].path));
|
|
||||||
m_activeText = players[activeIdx].name;
|
|
||||||
|
|
||||||
|
|
||||||
for (const auto &player : players) {
|
|
||||||
QIcon playerIcon = iconProvider.icon(QFileInfo(player.path));
|
|
||||||
bool isActive = (player.path == activePlayerPath);
|
|
||||||
QString title = isActive ? QString("★ %1").arg(player.name) : player.name;
|
|
||||||
|
|
||||||
|
|
||||||
auto *widgetAction = new QWidgetAction(m_menu);
|
|
||||||
|
|
||||||
auto *container = new QWidget();
|
|
||||||
container->setObjectName("stream-menu-item");
|
|
||||||
auto *layout = new QHBoxLayout(container);
|
|
||||||
layout->setContentsMargins(14, 6, 28, 6);
|
|
||||||
layout->setSpacing(10);
|
|
||||||
|
|
||||||
|
|
||||||
auto *iconLabel = new QLabel(container);
|
|
||||||
iconLabel->setFixedSize(20, 20);
|
|
||||||
iconLabel->setPixmap(playerIcon.pixmap(20, 20));
|
|
||||||
iconLabel->setStyleSheet("background: transparent;");
|
|
||||||
|
|
||||||
|
|
||||||
auto *textContainer = new QWidget(container);
|
|
||||||
auto *textLayout = new QVBoxLayout(textContainer);
|
|
||||||
textLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
textLayout->setSpacing(2);
|
|
||||||
|
|
||||||
auto *line1 = new QLabel(title, textContainer);
|
|
||||||
line1->setObjectName("stream-menu-line1");
|
|
||||||
|
|
||||||
auto *line2 = new QLabel(player.path, textContainer);
|
|
||||||
line2->setObjectName("stream-menu-line2");
|
|
||||||
|
|
||||||
textLayout->addWidget(line1);
|
|
||||||
textLayout->addWidget(line2);
|
|
||||||
|
|
||||||
layout->addWidget(iconLabel);
|
|
||||||
layout->addWidget(textContainer, 1);
|
|
||||||
|
|
||||||
widgetAction->setDefaultWidget(container);
|
|
||||||
widgetAction->setCheckable(true);
|
|
||||||
widgetAction->setChecked(isActive);
|
|
||||||
m_menu->addAction(widgetAction);
|
|
||||||
|
|
||||||
QString path = player.path;
|
|
||||||
connect(widgetAction, &QAction::triggered, this, [this, path]() {
|
|
||||||
emit playerSelected(path);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setToolTip(tr("Play with %1").arg(m_activeText));
|
|
||||||
show();
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SplitPlayerButton::setIconOnly(bool iconOnly) {
|
|
||||||
if (m_iconOnly == iconOnly) return;
|
|
||||||
m_iconOnly = iconOnly;
|
|
||||||
updateGeometry();
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SplitPlayerButton::clear() {
|
|
||||||
m_menu->clear();
|
|
||||||
m_activeIcon = QIcon();
|
|
||||||
m_activeText.clear();
|
|
||||||
m_activePath.clear();
|
|
||||||
m_hasMultiple = false;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
int SplitPlayerButton::arrowAreaWidth() const {
|
|
||||||
return m_hasMultiple ? 28 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
SplitPlayerButton::Zone SplitPlayerButton::zoneAt(const QPoint &pos) const {
|
|
||||||
if (!rect().contains(pos)) return None;
|
|
||||||
if (m_hasMultiple && pos.x() >= width() - arrowAreaWidth())
|
|
||||||
return ArrowZone;
|
|
||||||
return MainZone;
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize SplitPlayerButton::sizeHint() const {
|
|
||||||
if (m_iconOnly) {
|
|
||||||
|
|
||||||
int w = 8 + 20 + 8;
|
|
||||||
if (m_hasMultiple) w += arrowAreaWidth();
|
|
||||||
return QSize(w, 36);
|
|
||||||
}
|
|
||||||
int textWidth = fontMetrics().horizontalAdvance(m_activeText);
|
|
||||||
int w = 8 + 20 + 6 + textWidth + 8;
|
|
||||||
if (m_hasMultiple) w += arrowAreaWidth();
|
|
||||||
return QSize(qMax(w, 60), 36);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void SplitPlayerButton::paintEvent(QPaintEvent *) {
|
|
||||||
QPainter p(this);
|
|
||||||
p.setRenderHint(QPainter::Antialiasing);
|
|
||||||
p.setRenderHint(QPainter::SmoothPixmapTransform);
|
|
||||||
|
|
||||||
int arrowW = arrowAreaWidth();
|
|
||||||
QRectF mainRect(0, 0, width() - arrowW, height());
|
|
||||||
QRectF arrowRect(width() - arrowW, 0, arrowW, height());
|
|
||||||
QRectF fullRect(0, 0, width(), height());
|
|
||||||
|
|
||||||
|
|
||||||
QPainterPath bgPath;
|
|
||||||
bgPath.addRoundedRect(fullRect.adjusted(0.5, 0.5, -0.5, -0.5),
|
|
||||||
m_borderRadius, m_borderRadius);
|
|
||||||
|
|
||||||
QColor mainBg = Qt::transparent;
|
|
||||||
QColor arrowBg = Qt::transparent;
|
|
||||||
|
|
||||||
if (m_pressedZone == MainZone) mainBg = m_pressedColor;
|
|
||||||
else if (m_hoverZone == MainZone) mainBg = m_hoverColor;
|
|
||||||
|
|
||||||
if (m_pressedZone == ArrowZone) arrowBg = m_pressedColor;
|
|
||||||
else if (m_hoverZone == ArrowZone) arrowBg = m_hoverColor;
|
|
||||||
|
|
||||||
if (m_hasMultiple) {
|
|
||||||
p.save();
|
|
||||||
p.setClipPath(bgPath);
|
|
||||||
if (mainBg != Qt::transparent)
|
|
||||||
p.fillRect(mainRect, mainBg);
|
|
||||||
if (arrowBg != Qt::transparent)
|
|
||||||
p.fillRect(arrowRect, arrowBg);
|
|
||||||
p.restore();
|
|
||||||
} else {
|
|
||||||
if (mainBg != Qt::transparent) {
|
|
||||||
p.save();
|
|
||||||
p.setClipPath(bgPath);
|
|
||||||
p.fillRect(fullRect, mainBg);
|
|
||||||
p.restore();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int iconSize = 20;
|
|
||||||
int iconX = 8;
|
|
||||||
int iconY = (height() - iconSize) / 2;
|
|
||||||
m_activeIcon.paint(&p, iconX, iconY, iconSize, iconSize);
|
|
||||||
|
|
||||||
|
|
||||||
if (!m_iconOnly) {
|
|
||||||
int textX = iconX + iconSize + 6;
|
|
||||||
int textW = width() - textX - arrowW - 4;
|
|
||||||
if (textW > 0) {
|
|
||||||
p.setPen(m_textColor);
|
|
||||||
QFont f = font();
|
|
||||||
f.setPointSizeF(9.5);
|
|
||||||
p.setFont(f);
|
|
||||||
QString elidedText = p.fontMetrics().elidedText(
|
|
||||||
m_activeText, Qt::ElideRight, textW);
|
|
||||||
p.drawText(QRectF(textX, 0, textW, height()),
|
|
||||||
Qt::AlignVCenter | Qt::AlignLeft, elidedText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (m_hasMultiple) {
|
|
||||||
p.setPen(m_arrowColor);
|
|
||||||
QFont arrowFont = font();
|
|
||||||
arrowFont.setPointSizeF(9);
|
|
||||||
p.setFont(arrowFont);
|
|
||||||
p.drawText(arrowRect, Qt::AlignCenter, "⯆");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void SplitPlayerButton::mousePressEvent(QMouseEvent *e) {
|
|
||||||
if (e->button() == Qt::LeftButton)
|
|
||||||
m_pressedZone = zoneAt(e->pos());
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SplitPlayerButton::mouseReleaseEvent(QMouseEvent *e) {
|
|
||||||
if (e->button() == Qt::LeftButton) {
|
|
||||||
Zone released = zoneAt(e->pos());
|
|
||||||
if (released == m_pressedZone) {
|
|
||||||
if (released == MainZone && !m_activePath.isEmpty()) {
|
|
||||||
emit playRequested(m_activePath);
|
|
||||||
} else if (released == ArrowZone) {
|
|
||||||
showPlayerMenu();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_pressedZone = None;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SplitPlayerButton::mouseMoveEvent(QMouseEvent *e) {
|
|
||||||
Zone newZone = zoneAt(e->pos());
|
|
||||||
if (newZone != m_hoverZone) {
|
|
||||||
m_hoverZone = newZone;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SplitPlayerButton::enterEvent(QEnterEvent *) {
|
|
||||||
}
|
|
||||||
|
|
||||||
void SplitPlayerButton::leaveEvent(QEvent *) {
|
|
||||||
m_hoverZone = None;
|
|
||||||
m_pressedZone = None;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SplitPlayerButton::showPlayerMenu() {
|
|
||||||
|
|
||||||
m_menu->adjustSize();
|
|
||||||
QPoint pos = mapToGlobal(QPoint(width() / 2, height() + 4));
|
|
||||||
pos.setX(pos.x() - m_menu->sizeHint().width() / 2);
|
|
||||||
m_menu->exec(pos);
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
#ifndef SPLITPLAYERBUTTON_H
|
|
||||||
#define SPLITPLAYERBUTTON_H
|
|
||||||
|
|
||||||
#include <QWidget>
|
|
||||||
#include <QIcon>
|
|
||||||
#include <QMenu>
|
|
||||||
|
|
||||||
struct DetectedPlayer;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SplitPlayerButton : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
|
|
||||||
Q_PROPERTY(QColor hoverColor READ hoverColor WRITE setHoverColor)
|
|
||||||
Q_PROPERTY(QColor pressedColor READ pressedColor WRITE setPressedColor)
|
|
||||||
Q_PROPERTY(QColor separatorColor READ separatorColor WRITE setSeparatorColor)
|
|
||||||
Q_PROPERTY(QColor arrowColor READ arrowColor WRITE setArrowColor)
|
|
||||||
Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor)
|
|
||||||
Q_PROPERTY(int borderRadius READ borderRadius WRITE setBorderRadius)
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit SplitPlayerButton(QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
|
|
||||||
void setPlayers(const QList<DetectedPlayer> &players, const QString &activePlayerPath);
|
|
||||||
void clear();
|
|
||||||
|
|
||||||
|
|
||||||
void setIconOnly(bool iconOnly);
|
|
||||||
bool isIconOnly() const { return m_iconOnly; }
|
|
||||||
|
|
||||||
|
|
||||||
QColor hoverColor() const { return m_hoverColor; }
|
|
||||||
QColor pressedColor() const { return m_pressedColor; }
|
|
||||||
QColor separatorColor() const { return m_separatorColor; }
|
|
||||||
QColor arrowColor() const { return m_arrowColor; }
|
|
||||||
QColor textColor() const { return m_textColor; }
|
|
||||||
int borderRadius() const { return m_borderRadius; }
|
|
||||||
|
|
||||||
void setHoverColor (const QColor &c) { m_hoverColor = c; update(); }
|
|
||||||
void setPressedColor (const QColor &c) { m_pressedColor = c; update(); }
|
|
||||||
void setSeparatorColor(const QColor &c) { m_separatorColor = c; update(); }
|
|
||||||
void setArrowColor (const QColor &c) { m_arrowColor = c; update(); }
|
|
||||||
void setTextColor (const QColor &c) { m_textColor = c; update(); }
|
|
||||||
void setBorderRadius (int r) { m_borderRadius = r; update(); }
|
|
||||||
|
|
||||||
signals:
|
|
||||||
|
|
||||||
void playRequested(const QString &playerPath);
|
|
||||||
|
|
||||||
|
|
||||||
void playerSelected(const QString &playerPath);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void paintEvent(QPaintEvent *event) override;
|
|
||||||
void mousePressEvent(QMouseEvent *event) override;
|
|
||||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
|
||||||
void mouseMoveEvent(QMouseEvent *event) override;
|
|
||||||
void enterEvent(QEnterEvent *event) override;
|
|
||||||
void leaveEvent(QEvent *event) override;
|
|
||||||
QSize sizeHint() const override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
enum Zone { None, MainZone, ArrowZone };
|
|
||||||
|
|
||||||
Zone zoneAt(const QPoint &pos) const;
|
|
||||||
int arrowAreaWidth() const;
|
|
||||||
void showPlayerMenu();
|
|
||||||
|
|
||||||
|
|
||||||
QIcon m_activeIcon;
|
|
||||||
QString m_activeText;
|
|
||||||
QString m_activePath;
|
|
||||||
bool m_hasMultiple = false;
|
|
||||||
|
|
||||||
QMenu *m_menu;
|
|
||||||
|
|
||||||
Zone m_hoverZone = None;
|
|
||||||
Zone m_pressedZone = None;
|
|
||||||
|
|
||||||
|
|
||||||
QColor m_hoverColor {255, 255, 255, 30};
|
|
||||||
QColor m_pressedColor {255, 255, 255, 20};
|
|
||||||
QColor m_separatorColor {255, 255, 255, 50};
|
|
||||||
QColor m_arrowColor {255, 255, 255, 180};
|
|
||||||
QColor m_textColor {255, 255, 255, 180};
|
|
||||||
int m_borderRadius = 6;
|
|
||||||
bool m_iconOnly = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
#include "themetransitionwidget.h"
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QPainterPath>
|
|
||||||
|
|
||||||
ThemeTransitionWidget::ThemeTransitionWidget(const QPixmap& oldSnapshot, const QPoint& center, QWidget* parent)
|
|
||||||
: QWidget(parent), m_oldSnapshot(oldSnapshot), m_center(center), m_radius(0)
|
|
||||||
{
|
|
||||||
|
|
||||||
setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
||||||
|
|
||||||
setAttribute(Qt::WA_TranslucentBackground);
|
|
||||||
setAttribute(Qt::WA_NoSystemBackground);
|
|
||||||
}
|
|
||||||
|
|
||||||
int ThemeTransitionWidget::radius() const { return m_radius; }
|
|
||||||
|
|
||||||
void ThemeTransitionWidget::setRadius(int r) {
|
|
||||||
m_radius = r;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ThemeTransitionWidget::paintEvent(QPaintEvent* event) {
|
|
||||||
Q_UNUSED(event);
|
|
||||||
QPainter painter(this);
|
|
||||||
painter.setRenderHint(QPainter::Antialiasing);
|
|
||||||
|
|
||||||
|
|
||||||
QPainterPath path;
|
|
||||||
path.addRect(rect());
|
|
||||||
path.addEllipse(m_center, m_radius, m_radius);
|
|
||||||
path.setFillRule(Qt::OddEvenFill);
|
|
||||||
|
|
||||||
|
|
||||||
painter.setClipPath(path);
|
|
||||||
|
|
||||||
painter.drawPixmap(0, 0, m_oldSnapshot);
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
#ifndef THEMETRANSITIONWIDGET_H
|
|
||||||
#define THEMETRANSITIONWIDGET_H
|
|
||||||
|
|
||||||
#include <QWidget>
|
|
||||||
#include <QPixmap>
|
|
||||||
#include <QPoint>
|
|
||||||
|
|
||||||
class ThemeTransitionWidget : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
Q_PROPERTY(int radius READ radius WRITE setRadius)
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit ThemeTransitionWidget(const QPixmap& oldSnapshot, const QPoint& center, QWidget* parent = nullptr);
|
|
||||||
|
|
||||||
int radius() const;
|
|
||||||
void setRadius(int r);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void paintEvent(QPaintEvent* event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
QPixmap m_oldSnapshot;
|
|
||||||
QPoint m_center;
|
|
||||||
int m_radius;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
#include "transcodingcodeclistwidget.h"
|
|
||||||
|
|
||||||
#include "transcodingcodecrowwidget.h"
|
|
||||||
|
|
||||||
#include <QPropertyAnimation>
|
|
||||||
#include <QResizeEvent>
|
|
||||||
#include <QScrollArea>
|
|
||||||
#include <QScrollBar>
|
|
||||||
#include <QTimer>
|
|
||||||
#include "../tokens/AnimationTokens.h"
|
|
||||||
#include "../utils/motionprefs.h"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
void stopPosAnimations(TranscodingCodecRowWidget* row) {
|
|
||||||
const auto children = row->children();
|
|
||||||
for (auto* child : children) {
|
|
||||||
if (auto* animation = qobject_cast<QPropertyAnimation*>(child)) {
|
|
||||||
if (animation->propertyName() == "pos") {
|
|
||||||
animation->stop();
|
|
||||||
animation->deleteLater();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
TranscodingCodecListWidget::TranscodingCodecListWidget(QWidget* parent) : QWidget(parent) {
|
|
||||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
|
||||||
|
|
||||||
m_autoScrollTimer = new QTimer(this);
|
|
||||||
m_autoScrollTimer->setInterval(16);
|
|
||||||
connect(m_autoScrollTimer, &QTimer::timeout, this, [this]() {
|
|
||||||
if (!m_dragRow) {
|
|
||||||
m_autoScrollTimer->stop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* scrollArea = parentScrollArea();
|
|
||||||
if (!scrollArea) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* scrollBar = scrollArea->verticalScrollBar();
|
|
||||||
const int delta = autoScrollDelta(m_lastDragGlobalPos);
|
|
||||||
if (delta == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int oldValue = scrollBar->value();
|
|
||||||
scrollBar->setValue(
|
|
||||||
qBound(scrollBar->minimum(), oldValue + delta, scrollBar->maximum()));
|
|
||||||
if (scrollBar->value() != oldValue) {
|
|
||||||
updateDragPosition(m_dragRow, m_lastDragGlobalPos);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void TranscodingCodecListWidget::setCodecRows(
|
|
||||||
const QList<TranscodingCodecRowWidget*>& rows) {
|
|
||||||
for (auto* row : m_rows) {
|
|
||||||
if (row && !rows.contains(row)) {
|
|
||||||
row->hide();
|
|
||||||
row->deleteLater();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_rows = rows;
|
|
||||||
for (auto* row : m_rows) {
|
|
||||||
if (!row) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
row->setParent(this);
|
|
||||||
row->show();
|
|
||||||
connect(row, &TranscodingCodecRowWidget::dragStarted, this,
|
|
||||||
&TranscodingCodecListWidget::onRowDragStart, Qt::UniqueConnection);
|
|
||||||
connect(row, &TranscodingCodecRowWidget::dragMoved, this,
|
|
||||||
&TranscodingCodecListWidget::onRowDragMove, Qt::UniqueConnection);
|
|
||||||
connect(row, &TranscodingCodecRowWidget::dragFinished, this,
|
|
||||||
&TranscodingCodecListWidget::onRowDragFinish, Qt::UniqueConnection);
|
|
||||||
}
|
|
||||||
|
|
||||||
relayout(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList TranscodingCodecListWidget::codecOrder() const {
|
|
||||||
QStringList codecIds;
|
|
||||||
codecIds.reserve(m_rows.size());
|
|
||||||
for (auto* row : m_rows) {
|
|
||||||
if (row) {
|
|
||||||
codecIds.append(row->codecId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return codecIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TranscodingCodecListWidget::onRowDragStart(TranscodingCodecRowWidget* row,
|
|
||||||
const QPoint& globalPos) {
|
|
||||||
if (!row) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_dragRow = row;
|
|
||||||
m_dragOriginalIndex = m_rows.indexOf(row);
|
|
||||||
if (m_dragOriginalIndex < 0) {
|
|
||||||
m_dragRow = nullptr;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_dragCurrentIndex = m_dragOriginalIndex;
|
|
||||||
m_lastDragGlobalPos = globalPos;
|
|
||||||
m_dragHotSpotY = row->mapFromGlobal(globalPos).y();
|
|
||||||
|
|
||||||
for (auto* currentRow : m_rows) {
|
|
||||||
if (currentRow) {
|
|
||||||
stopPosAnimations(currentRow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
row->setDragging(true);
|
|
||||||
row->raise();
|
|
||||||
m_autoScrollTimer->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void TranscodingCodecListWidget::onRowDragMove(TranscodingCodecRowWidget* row,
|
|
||||||
const QPoint& globalPos) {
|
|
||||||
if (!row || row != m_dragRow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_lastDragGlobalPos = globalPos;
|
|
||||||
updateDragPosition(row, globalPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TranscodingCodecListWidget::onRowDragFinish(TranscodingCodecRowWidget* row) {
|
|
||||||
if (!row || row != m_dragRow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
row->setDragging(false);
|
|
||||||
m_autoScrollTimer->stop();
|
|
||||||
|
|
||||||
const int finalIndex = qMax(0, m_dragCurrentIndex);
|
|
||||||
const bool changed = (m_dragOriginalIndex != m_dragCurrentIndex);
|
|
||||||
|
|
||||||
m_dragRow = nullptr;
|
|
||||||
m_dragOriginalIndex = -1;
|
|
||||||
m_dragCurrentIndex = -1;
|
|
||||||
m_dragHotSpotY = 0;
|
|
||||||
|
|
||||||
stopPosAnimations(row);
|
|
||||||
if (MotionPrefs::reduceMotion()) {
|
|
||||||
row->move(QPoint(0, rowTopForIndex(finalIndex)));
|
|
||||||
} else {
|
|
||||||
auto* animation = new QPropertyAnimation(row, "pos", row);
|
|
||||||
animation->setDuration(AnimationTokens::DurationMs::kFast);
|
|
||||||
animation->setEasingCurve(AnimationTokens::Curve::enter());
|
|
||||||
animation->setEndValue(QPoint(0, rowTopForIndex(finalIndex)));
|
|
||||||
animation->start(QAbstractAnimation::DeleteWhenStopped);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
emit orderChanged(codecOrder());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void TranscodingCodecListWidget::resizeEvent(QResizeEvent* event) {
|
|
||||||
QWidget::resizeEvent(event);
|
|
||||||
relayout(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
int TranscodingCodecListWidget::rowHeightFor(TranscodingCodecRowWidget* row) const {
|
|
||||||
return row ? row->sizeHint().height() : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int TranscodingCodecListWidget::rowTopForIndex(int index) const {
|
|
||||||
int top = 0;
|
|
||||||
for (int i = 0; i < index && i < m_rows.size(); ++i) {
|
|
||||||
top += rowHeightFor(m_rows.at(i)) + RowSpacing;
|
|
||||||
}
|
|
||||||
return top;
|
|
||||||
}
|
|
||||||
|
|
||||||
int TranscodingCodecListWidget::indexForCenterY(int centerY) const {
|
|
||||||
QList<TranscodingCodecRowWidget*> orderedRows = m_rows;
|
|
||||||
orderedRows.removeOne(m_dragRow);
|
|
||||||
|
|
||||||
int top = 0;
|
|
||||||
for (int index = 0; index < orderedRows.size(); ++index) {
|
|
||||||
const int rowHeight = rowHeightFor(orderedRows.at(index));
|
|
||||||
if (centerY < top + rowHeight / 2) {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
top += rowHeight + RowSpacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
return orderedRows.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
QScrollArea* TranscodingCodecListWidget::parentScrollArea() const {
|
|
||||||
QWidget* current = parentWidget();
|
|
||||||
while (current) {
|
|
||||||
if (auto* scrollArea = qobject_cast<QScrollArea*>(current)) {
|
|
||||||
return scrollArea;
|
|
||||||
}
|
|
||||||
current = current->parentWidget();
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
int TranscodingCodecListWidget::autoScrollDelta(const QPoint& globalPos) const {
|
|
||||||
auto* scrollArea = parentScrollArea();
|
|
||||||
if (!scrollArea) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* scrollBar = scrollArea->verticalScrollBar();
|
|
||||||
if (!scrollBar || scrollBar->maximum() <= scrollBar->minimum()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QRect viewportRect(scrollArea->viewport()->mapToGlobal(QPoint(0, 0)),
|
|
||||||
scrollArea->viewport()->size());
|
|
||||||
static constexpr int EdgeThreshold = 56;
|
|
||||||
static constexpr int MaxScrollStep = 22;
|
|
||||||
|
|
||||||
const int topHotZone = viewportRect.top() + EdgeThreshold;
|
|
||||||
if (globalPos.y() < topHotZone) {
|
|
||||||
const int distance = topHotZone - globalPos.y();
|
|
||||||
return -qBound(4, distance / 2, MaxScrollStep);
|
|
||||||
}
|
|
||||||
|
|
||||||
const int bottomHotZone = viewportRect.bottom() - EdgeThreshold;
|
|
||||||
if (globalPos.y() > bottomHotZone) {
|
|
||||||
const int distance = globalPos.y() - bottomHotZone;
|
|
||||||
return qBound(4, distance / 2, MaxScrollStep);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TranscodingCodecListWidget::updateDragPosition(TranscodingCodecRowWidget* row,
|
|
||||||
const QPoint& globalPos) {
|
|
||||||
if (!row) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QPoint localPos = mapFromGlobal(globalPos);
|
|
||||||
const int rowY = localPos.y() - m_dragHotSpotY;
|
|
||||||
row->move(0, rowY);
|
|
||||||
|
|
||||||
const int centerY = rowY + row->height() / 2;
|
|
||||||
const int targetIndex = indexForCenterY(centerY);
|
|
||||||
if (targetIndex == m_dragCurrentIndex) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_rows.removeOne(row);
|
|
||||||
m_rows.insert(qBound(0, targetIndex, m_rows.size()), row);
|
|
||||||
m_dragCurrentIndex = m_rows.indexOf(row);
|
|
||||||
relayout(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TranscodingCodecListWidget::relayout(bool animate) {
|
|
||||||
int top = 0;
|
|
||||||
const int contentWidth = qMax(0, width());
|
|
||||||
|
|
||||||
for (int index = 0; index < m_rows.size(); ++index) {
|
|
||||||
auto* row = m_rows.at(index);
|
|
||||||
if (!row) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int rowHeight = rowHeightFor(row);
|
|
||||||
row->setFixedSize(contentWidth, rowHeight);
|
|
||||||
|
|
||||||
if (row == m_dragRow) {
|
|
||||||
top += rowHeight + RowSpacing;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QPoint targetPos(0, top);
|
|
||||||
if (animate && !MotionPrefs::reduceMotion() && row->pos() != targetPos) {
|
|
||||||
stopPosAnimations(row);
|
|
||||||
|
|
||||||
auto* animation = new QPropertyAnimation(row, "pos", row);
|
|
||||||
animation->setDuration(AnimationTokens::DurationMs::kNormal);
|
|
||||||
animation->setEasingCurve(AnimationTokens::Curve::enter());
|
|
||||||
animation->setEndValue(targetPos);
|
|
||||||
animation->start(QAbstractAnimation::DeleteWhenStopped);
|
|
||||||
} else {
|
|
||||||
stopPosAnimations(row);
|
|
||||||
row->move(targetPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
top += rowHeight + RowSpacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (top > 0) {
|
|
||||||
top -= RowSpacing;
|
|
||||||
}
|
|
||||||
setFixedHeight(qMax(top, 0));
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
#ifndef TRANSCODINGCODECLISTWIDGET_H
|
|
||||||
#define TRANSCODINGCODECLISTWIDGET_H
|
|
||||||
|
|
||||||
#include <QList>
|
|
||||||
#include <QPoint>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QWidget>
|
|
||||||
|
|
||||||
class QResizeEvent;
|
|
||||||
class QScrollArea;
|
|
||||||
class QTimer;
|
|
||||||
class TranscodingCodecRowWidget;
|
|
||||||
|
|
||||||
class TranscodingCodecListWidget : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit TranscodingCodecListWidget(QWidget* parent = nullptr);
|
|
||||||
|
|
||||||
void setCodecRows(const QList<TranscodingCodecRowWidget*>& rows);
|
|
||||||
QStringList codecOrder() const;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void orderChanged(const QStringList& codecIds);
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
void onRowDragStart(TranscodingCodecRowWidget* row, const QPoint& globalPos);
|
|
||||||
void onRowDragMove(TranscodingCodecRowWidget* row, const QPoint& globalPos);
|
|
||||||
void onRowDragFinish(TranscodingCodecRowWidget* row);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void resizeEvent(QResizeEvent* event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
static constexpr int RowSpacing = 10;
|
|
||||||
|
|
||||||
int rowHeightFor(TranscodingCodecRowWidget* row) const;
|
|
||||||
int rowTopForIndex(int index) const;
|
|
||||||
int indexForCenterY(int centerY) const;
|
|
||||||
QScrollArea* parentScrollArea() const;
|
|
||||||
int autoScrollDelta(const QPoint& globalPos) const;
|
|
||||||
void updateDragPosition(TranscodingCodecRowWidget* row, const QPoint& globalPos);
|
|
||||||
void relayout(bool animate = false);
|
|
||||||
|
|
||||||
QList<TranscodingCodecRowWidget*> m_rows;
|
|
||||||
TranscodingCodecRowWidget* m_dragRow = nullptr;
|
|
||||||
int m_dragOriginalIndex = -1;
|
|
||||||
int m_dragCurrentIndex = -1;
|
|
||||||
int m_dragHotSpotY = 0;
|
|
||||||
QPoint m_lastDragGlobalPos;
|
|
||||||
QTimer* m_autoScrollTimer = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
#include "transcodingcodecrowwidget.h"
|
|
||||||
|
|
||||||
#include "elidedlabel.h"
|
|
||||||
#include "../managers/thememanager.h"
|
|
||||||
#include "modernswitch.h"
|
|
||||||
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QEvent>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QMouseEvent>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QStyle>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
TranscodingCodecRowWidget::TranscodingCodecRowWidget(const QString& codecId,
|
|
||||||
const QString& title,
|
|
||||||
const QString& description,
|
|
||||||
const QString& infoToolTip,
|
|
||||||
const QString& dragToolTip,
|
|
||||||
QWidget* parent)
|
|
||||||
: QFrame(parent), m_codecId(codecId) {
|
|
||||||
setObjectName("TranscodingCodecItem");
|
|
||||||
setAttribute(Qt::WA_StyledBackground, true);
|
|
||||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
|
||||||
|
|
||||||
auto* rowLayout = new QHBoxLayout(this);
|
|
||||||
rowLayout->setContentsMargins(14, 12, 14, 12);
|
|
||||||
rowLayout->setSpacing(12);
|
|
||||||
|
|
||||||
auto* textLayout = new QVBoxLayout();
|
|
||||||
textLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
textLayout->setSpacing(3);
|
|
||||||
|
|
||||||
m_titleLabel = new ElidedLabel(this);
|
|
||||||
m_titleLabel->setObjectName("TranscodingCodecName");
|
|
||||||
m_titleLabel->setFullText(title);
|
|
||||||
textLayout->addWidget(m_titleLabel);
|
|
||||||
|
|
||||||
m_descriptionLabel = new ElidedLabel(this);
|
|
||||||
m_descriptionLabel->setObjectName("TranscodingCodecDescription");
|
|
||||||
m_descriptionLabel->setFullText(description);
|
|
||||||
m_descriptionLabel->setVisible(!description.trimmed().isEmpty());
|
|
||||||
textLayout->addWidget(m_descriptionLabel);
|
|
||||||
|
|
||||||
rowLayout->addLayout(textLayout, 1);
|
|
||||||
|
|
||||||
auto* controlsLayout = new QHBoxLayout();
|
|
||||||
controlsLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
controlsLayout->setSpacing(8);
|
|
||||||
|
|
||||||
m_infoButton = new QPushButton(this);
|
|
||||||
m_infoButton->setObjectName("TranscodingMiniButton");
|
|
||||||
m_infoButton->setCursor(Qt::PointingHandCursor);
|
|
||||||
m_infoButton->setFixedSize(30, 30);
|
|
||||||
m_infoButton->setIconSize(QSize(16, 16));
|
|
||||||
m_infoButton->setIcon(ThemeManager::getAdaptiveIcon(QStringLiteral(":/svg/player/info.svg")));
|
|
||||||
m_infoButton->setToolTip(infoToolTip);
|
|
||||||
m_infoButton->setFocusPolicy(Qt::NoFocus);
|
|
||||||
controlsLayout->addWidget(m_infoButton, 0, Qt::AlignVCenter);
|
|
||||||
|
|
||||||
m_dragHandle = new QPushButton(this);
|
|
||||||
m_dragHandle->setObjectName("TranscodingDragHandle");
|
|
||||||
m_dragHandle->setCursor(Qt::OpenHandCursor);
|
|
||||||
m_dragHandle->setFixedSize(30, 30);
|
|
||||||
m_dragHandle->setToolTip(dragToolTip);
|
|
||||||
m_dragHandle->setFocusPolicy(Qt::NoFocus);
|
|
||||||
m_dragHandle->installEventFilter(this);
|
|
||||||
controlsLayout->addWidget(m_dragHandle, 0, Qt::AlignVCenter);
|
|
||||||
|
|
||||||
m_switch = new ModernSwitch(this);
|
|
||||||
controlsLayout->addWidget(m_switch, 0, Qt::AlignVCenter);
|
|
||||||
|
|
||||||
rowLayout->addLayout(controlsLayout, 0);
|
|
||||||
|
|
||||||
connect(m_infoButton, &QPushButton::clicked, this,
|
|
||||||
[this]() { emit infoRequested(m_codecId); });
|
|
||||||
connect(m_switch, &ModernSwitch::toggled, this, [this](bool enabled) {
|
|
||||||
emit enabledChanged(m_codecId, enabled);
|
|
||||||
});
|
|
||||||
connect(ThemeManager::instance(), &ThemeManager::themeChanged, this, [this]() {
|
|
||||||
if (m_infoButton) {
|
|
||||||
m_infoButton->setIcon(
|
|
||||||
ThemeManager::getAdaptiveIcon(QStringLiteral(":/svg/player/info.svg")));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TranscodingCodecRowWidget::isCodecEnabled() const {
|
|
||||||
return m_switch && m_switch->isChecked();
|
|
||||||
}
|
|
||||||
|
|
||||||
void TranscodingCodecRowWidget::setCodecEnabled(bool enabled) {
|
|
||||||
if (m_switch) {
|
|
||||||
m_switch->setChecked(enabled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void TranscodingCodecRowWidget::setDragging(bool dragging) {
|
|
||||||
setProperty("dragging", dragging);
|
|
||||||
style()->unpolish(this);
|
|
||||||
style()->polish(this);
|
|
||||||
update();
|
|
||||||
|
|
||||||
if (m_dragHandle) {
|
|
||||||
m_dragHandle->setCursor(dragging ? Qt::ClosedHandCursor : Qt::OpenHandCursor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize TranscodingCodecRowWidget::sizeHint() const {
|
|
||||||
return QSize(420, 68);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TranscodingCodecRowWidget::eventFilter(QObject* watched, QEvent* event) {
|
|
||||||
if (watched != m_dragHandle) {
|
|
||||||
return QFrame::eventFilter(watched, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event->type()) {
|
|
||||||
case QEvent::MouseButtonPress: {
|
|
||||||
auto* mouseEvent = static_cast<QMouseEvent*>(event);
|
|
||||||
if (mouseEvent->button() != Qt::LeftButton) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_dragCandidate = true;
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
|
||||||
m_dragStartPos = mouseEvent->globalPosition().toPoint();
|
|
||||||
#else
|
|
||||||
m_dragStartPos = mouseEvent->globalPos();
|
|
||||||
#endif
|
|
||||||
m_dragHandle->setCursor(Qt::ClosedHandCursor);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case QEvent::MouseMove: {
|
|
||||||
if (!m_dragCandidate) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* mouseEvent = static_cast<QMouseEvent*>(event);
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
|
||||||
const QPoint globalPos = mouseEvent->globalPosition().toPoint();
|
|
||||||
#else
|
|
||||||
const QPoint globalPos = mouseEvent->globalPos();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (!m_dragActive &&
|
|
||||||
(globalPos - m_dragStartPos).manhattanLength() >=
|
|
||||||
QApplication::startDragDistance()) {
|
|
||||||
m_dragActive = true;
|
|
||||||
emit dragStarted(this, globalPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_dragActive) {
|
|
||||||
emit dragMoved(this, globalPos);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case QEvent::MouseButtonRelease: {
|
|
||||||
auto* mouseEvent = static_cast<QMouseEvent*>(event);
|
|
||||||
if (mouseEvent->button() != Qt::LeftButton) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool hadActiveDrag = m_dragActive;
|
|
||||||
m_dragCandidate = false;
|
|
||||||
m_dragActive = false;
|
|
||||||
m_dragHandle->setCursor(Qt::OpenHandCursor);
|
|
||||||
|
|
||||||
if (hadActiveDrag) {
|
|
||||||
emit dragFinished(this);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return QFrame::eventFilter(watched, event);
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
#ifndef TRANSCODINGCODECROWWIDGET_H
|
|
||||||
#define TRANSCODINGCODECROWWIDGET_H
|
|
||||||
|
|
||||||
#include <QFrame>
|
|
||||||
#include <QPoint>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
class ElidedLabel;
|
|
||||||
class ModernSwitch;
|
|
||||||
class QPushButton;
|
|
||||||
|
|
||||||
class TranscodingCodecRowWidget : public QFrame {
|
|
||||||
Q_OBJECT
|
|
||||||
Q_PROPERTY(QPoint pos READ pos WRITE move)
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit TranscodingCodecRowWidget(const QString& codecId,
|
|
||||||
const QString& title,
|
|
||||||
const QString& description,
|
|
||||||
const QString& infoToolTip,
|
|
||||||
const QString& dragToolTip,
|
|
||||||
QWidget* parent = nullptr);
|
|
||||||
|
|
||||||
QString codecId() const { return m_codecId; }
|
|
||||||
bool isCodecEnabled() const;
|
|
||||||
void setCodecEnabled(bool enabled);
|
|
||||||
void setDragging(bool dragging);
|
|
||||||
QSize sizeHint() const override;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void infoRequested(const QString& codecId);
|
|
||||||
void enabledChanged(const QString& codecId, bool enabled);
|
|
||||||
void dragStarted(TranscodingCodecRowWidget* row, const QPoint& globalPos);
|
|
||||||
void dragMoved(TranscodingCodecRowWidget* row, const QPoint& globalPos);
|
|
||||||
void dragFinished(TranscodingCodecRowWidget* row);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool eventFilter(QObject* watched, QEvent* event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
QString m_codecId;
|
|
||||||
ElidedLabel* m_titleLabel = nullptr;
|
|
||||||
ElidedLabel* m_descriptionLabel = nullptr;
|
|
||||||
QPushButton* m_infoButton = nullptr;
|
|
||||||
QPushButton* m_dragHandle = nullptr;
|
|
||||||
ModernSwitch* m_switch = nullptr;
|
|
||||||
QPoint m_dragStartPos;
|
|
||||||
bool m_dragCandidate = false;
|
|
||||||
bool m_dragActive = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
+80
-28
@@ -1,20 +1,30 @@
|
|||||||
#include "mainwindow.h"
|
#include "quick/appshellhost.h"
|
||||||
|
#include "quick/mpvitem.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
|
#include <QImage>
|
||||||
#include <QLocale>
|
#include <QLocale>
|
||||||
|
#include <QQmlApplicationEngine>
|
||||||
|
#include <QQuickItem>
|
||||||
|
#include <QQuickWindow>
|
||||||
#include <QSurfaceFormat>
|
#include <QSurfaceFormat>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
|
#include <QTimer>
|
||||||
#include <QTranslator>
|
#include <QTranslator>
|
||||||
|
|
||||||
#include "api/proxymanager.h"
|
#include "api/proxymanager.h"
|
||||||
#include "components/mpvcontroller.h"
|
#include "components/mpvcontroller.h"
|
||||||
#include "managers/languagemanager.h"
|
#include "managers/languagemanager.h"
|
||||||
#include "managers/logmanager.h"
|
#include "managers/logmanager.h"
|
||||||
|
|
||||||
|
#include <qembycore.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
QSurfaceFormat createDefaultSurfaceFormat() {
|
QSurfaceFormat createDefaultSurfaceFormat()
|
||||||
|
{
|
||||||
QSurfaceFormat format;
|
QSurfaceFormat format;
|
||||||
format.setRenderableType(QSurfaceFormat::OpenGL);
|
format.setRenderableType(QSurfaceFormat::OpenGL);
|
||||||
format.setVersion(3, 3);
|
format.setVersion(3, 3);
|
||||||
@@ -28,7 +38,8 @@ QSurfaceFormat createDefaultSurfaceFormat() {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
#if defined(Q_OS_LINUX)
|
#if defined(Q_OS_LINUX)
|
||||||
const QByteArray qpaPlatform = qgetenv("QT_QPA_PLATFORM");
|
const QByteArray qpaPlatform = qgetenv("QT_QPA_PLATFORM");
|
||||||
const QByteArray forceXcb = qgetenv("QEMBY_FORCE_XCB").toLower();
|
const QByteArray forceXcb = qgetenv("QEMBY_FORCE_XCB").toLower();
|
||||||
@@ -40,7 +51,13 @@ int main(int argc, char *argv[]) {
|
|||||||
|
|
||||||
QSurfaceFormat::setDefaultFormat(createDefaultSurfaceFormat());
|
QSurfaceFormat::setDefaultFormat(createDefaultSurfaceFormat());
|
||||||
|
|
||||||
|
// MpvItem uses QQuickFramebufferObject and requires the OpenGL scene graph
|
||||||
|
// backend; lock it before constructing QApplication.
|
||||||
|
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);
|
||||||
|
|
||||||
QGuiApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
|
QGuiApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
|
||||||
|
// QApplication (not QGuiApplication): remaining Widget dialogs and tray
|
||||||
|
// still need it. Independent playback is now a secondary QML window.
|
||||||
QApplication a(argc, argv);
|
QApplication a(argc, argv);
|
||||||
a.setApplicationName(APP_NAME);
|
a.setApplicationName(APP_NAME);
|
||||||
a.setApplicationVersion(APP_VERSION);
|
a.setApplicationVersion(APP_VERSION);
|
||||||
@@ -55,32 +72,67 @@ int main(int argc, char *argv[]) {
|
|||||||
LogManager::instance()->init();
|
LogManager::instance()->init();
|
||||||
LanguageManager::instance()->init();
|
LanguageManager::instance()->init();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ProxyManager::installApplicationFactory();
|
ProxyManager::installApplicationFactory();
|
||||||
|
|
||||||
MainWindow w;
|
// Spike harness: QEMBY_MPV_SPIKE=<media path> loads only MpvItem verification.
|
||||||
#if !defined(Q_OS_MACOS) && !defined(Q_OS_MAC)
|
const QByteArray spikeMedia = qgetenv("QEMBY_MPV_SPIKE");
|
||||||
w.setWindowIcon(appIcon);
|
if (!spikeMedia.isEmpty()) {
|
||||||
#endif
|
QQmlApplicationEngine engine;
|
||||||
w.show();
|
engine.loadFromModule("qEmby", "MpvSpikeHarness");
|
||||||
|
if (engine.rootObjects().isEmpty()) {
|
||||||
|
qCritical() << "[spike] failed to load MpvSpikeHarness";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
auto *win = qobject_cast<QQuickWindow *>(engine.rootObjects().constFirst());
|
||||||
|
if (auto *page = win ? win->findChild<QQuickItem *>(QStringLiteral("spikePage"))
|
||||||
|
: nullptr) {
|
||||||
|
const QString media = QString::fromLocal8Bit(spikeMedia);
|
||||||
|
page->setProperty("mediaUrl", media);
|
||||||
|
QTimer::singleShot(300, page, [page, media] {
|
||||||
|
auto *mpv = page->findChild<MpvItem *>();
|
||||||
|
if (!mpv) {
|
||||||
|
qWarning() << "[spike] MpvItem not found";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mpv->loadMedia(media);
|
||||||
|
mpv->play();
|
||||||
|
|
||||||
|
const int seekTo = qEnvironmentVariableIntValue("QEMBY_MPV_SPIKE_SEEK");
|
||||||
|
if (seekTo > 0)
|
||||||
|
QTimer::singleShot(2000, mpv, [mpv, seekTo] { mpv->seek(seekTo); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const int grabDelay = qEnvironmentVariableIntValue("QEMBY_MPV_SPIKE_GRAB_MS");
|
||||||
|
const QByteArray grabPath = qgetenv("QEMBY_MPV_SPIKE_GRAB");
|
||||||
|
if (win && !grabPath.isEmpty()) {
|
||||||
|
QTimer::singleShot(grabDelay > 0 ? grabDelay : 3000, win, [win, grabPath] {
|
||||||
|
const QImage shot = win->grabWindow();
|
||||||
|
if (shot.isNull())
|
||||||
|
qWarning() << "[spike] grabWindow returned null";
|
||||||
|
else if (!shot.save(QString::fromLocal8Bit(grabPath)))
|
||||||
|
qWarning() << "[spike] failed to save" << grabPath;
|
||||||
|
else
|
||||||
|
qInfo() << "[spike] saved frame to" << grabPath;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const int quitMs = qEnvironmentVariableIntValue("QEMBY_MPV_SPIKE_QUIT_MS");
|
||||||
|
if (quitMs > 0)
|
||||||
|
QTimer::singleShot(quitMs, &a, &QApplication::quit);
|
||||||
|
return a.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full QML application shell only (Widgets MainWindow removed).
|
||||||
|
auto *core = new QEmbyCore(&a);
|
||||||
|
auto *shell = new AppShellHost(core, &a);
|
||||||
|
if (!shell->load()) {
|
||||||
|
qCritical() << "[qml-shell] failed to load AppShell";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
MpvController::warmupOnce();
|
MpvController::warmupOnce();
|
||||||
|
const int rc = a.exec();
|
||||||
int ret = a.exec();
|
// Destroy the QML engine only after the event loop has fully unwound
|
||||||
|
// (not from aboutToQuit inside Window.closeEvent). ViewModels still live
|
||||||
return ret;
|
// as children of `shell` until `a` is destroyed below.
|
||||||
|
shell->shutdownUi();
|
||||||
|
return rc;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,68 +0,0 @@
|
|||||||
#ifndef MAINWINDOW_H
|
|
||||||
#define MAINWINDOW_H
|
|
||||||
|
|
||||||
#include <QMainWindow>
|
|
||||||
#include <QPointer>
|
|
||||||
#include <QElapsedTimer>
|
|
||||||
#include <QCloseEvent>
|
|
||||||
|
|
||||||
class QEmbyCore;
|
|
||||||
class LoginView;
|
|
||||||
class HomeView;
|
|
||||||
class QStackedWidget;
|
|
||||||
class QLineEdit;
|
|
||||||
class QCompleter;
|
|
||||||
class QStringListModel;
|
|
||||||
class TrayManager;
|
|
||||||
class SearchHistoryPopup;
|
|
||||||
|
|
||||||
class MainWindow : public QMainWindow
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
MainWindow(QWidget *parent = nullptr);
|
|
||||||
~MainWindow();
|
|
||||||
|
|
||||||
public Q_SLOTS:
|
|
||||||
void navigateToHome();
|
|
||||||
void navigateToLogin();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
|
||||||
void closeEvent(QCloseEvent *event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void requestApplicationQuit();
|
|
||||||
bool stopActivePlaybackForQuit();
|
|
||||||
void finishApplicationQuit();
|
|
||||||
void setupGlobalSearchHistory();
|
|
||||||
void hideGlobalSearchTransientUi();
|
|
||||||
void updateGlobalSearchCompleter(const QString &text = QString());
|
|
||||||
void showGlobalSearchHistoryPopup(const QString &filterText = QString());
|
|
||||||
void submitGlobalSearch(const QString &query);
|
|
||||||
QString currentSearchServerId() const;
|
|
||||||
|
|
||||||
QEmbyCore *m_core;
|
|
||||||
QStackedWidget *m_viewStack;
|
|
||||||
LoginView *m_loginView;
|
|
||||||
HomeView *m_homeView;
|
|
||||||
QPointer<QLineEdit> m_globalSearchBox;
|
|
||||||
QCompleter *m_globalSearchCompleter = nullptr;
|
|
||||||
QStringListModel *m_globalSearchModel = nullptr;
|
|
||||||
SearchHistoryPopup *m_globalSearchHistoryPopup = nullptr;
|
|
||||||
TrayManager *m_trayManager = nullptr;
|
|
||||||
|
|
||||||
|
|
||||||
QElapsedTimer m_backClickTimer;
|
|
||||||
|
|
||||||
quint32 m_defaultWidth{1280};
|
|
||||||
quint32 m_defaultHeight{800};
|
|
||||||
bool m_realQuit{false};
|
|
||||||
bool m_themeAnimating{false};
|
|
||||||
bool m_wasPausedByTray{false};
|
|
||||||
bool m_viewTransitionInProgress{false};
|
|
||||||
bool m_quitPending{false};
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
#include <config/configstore.h>
|
#include <config/configstore.h>
|
||||||
#include <config/config_keys.h>
|
#include <config/config_keys.h>
|
||||||
#include <models/media/playbackinfo.h>
|
#include <models/media/playbackinfo.h>
|
||||||
#include "../components/playerwindow.h"
|
#include "../quick/independentplayerhost.h"
|
||||||
#include "../components/moderntoast.h"
|
#include "../components/moderntoast.h"
|
||||||
#include "../utils/logredactionutils.h"
|
#include "../utils/logredactionutils.h"
|
||||||
#include "../utils/playerpreferenceutils.h"
|
#include "../utils/playerpreferenceutils.h"
|
||||||
@@ -39,16 +39,21 @@ void PlaybackManager::init(QEmbyCore* core)
|
|||||||
m_core = core;
|
m_core = core;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlaybackManager::setEmbeddedHostAvailable(bool available)
|
||||||
|
{
|
||||||
|
m_embeddedHostAvailable = available;
|
||||||
|
}
|
||||||
|
|
||||||
void PlaybackManager::startPlayback(const QString& mediaId, const QString& title,
|
void PlaybackManager::startPlayback(const QString& mediaId, const QString& title,
|
||||||
const QString& streamUrl, long long startPositionTicks,
|
const QString& streamUrl, long long startPositionTicks,
|
||||||
const QVariant& extraData)
|
const QVariant& extraData)
|
||||||
{
|
{
|
||||||
if (!m_core) return;
|
if (!m_core) return;
|
||||||
|
|
||||||
|
// Drop any existing independent window before starting a new session
|
||||||
if (m_independentWindow) {
|
// (embedded, external, or a replacement independent play).
|
||||||
m_independentWindow->close();
|
if (m_independentHost) {
|
||||||
|
m_independentHost->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
qDebug() << "[PlaybackManager] ===== startPlayback =====";
|
qDebug() << "[PlaybackManager] ===== startPlayback =====";
|
||||||
@@ -88,6 +93,13 @@ void PlaybackManager::startPlayback(const QString& mediaId, const QString& title
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// No embedded host: fall back to QML IndependentPlayerHost.
|
||||||
|
if (!m_embeddedHostAvailable) {
|
||||||
|
qDebug() << "[PlaybackManager] → Mode: Independent window (no embedded host)";
|
||||||
|
launchIndependentWindow(mediaId, title, streamUrl, startPositionTicks, extraData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
qDebug() << "[PlaybackManager] → Mode: Embedded player";
|
qDebug() << "[PlaybackManager] → Mode: Embedded player";
|
||||||
Q_EMIT requestEmbeddedPlay(mediaId, title, streamUrl, startPositionTicks, extraData);
|
Q_EMIT requestEmbeddedPlay(mediaId, title, streamUrl, startPositionTicks, extraData);
|
||||||
}
|
}
|
||||||
@@ -98,22 +110,25 @@ void PlaybackManager::startInternalPlayback(const QString& mediaId, const QStrin
|
|||||||
{
|
{
|
||||||
if (!m_core) return;
|
if (!m_core) return;
|
||||||
|
|
||||||
|
if (m_independentHost) {
|
||||||
if (m_independentWindow) {
|
m_independentHost->close();
|
||||||
m_independentWindow->close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
qDebug() << "[PlaybackManager] ===== startInternalPlayback =====";
|
qDebug() << "[PlaybackManager] ===== startInternalPlayback =====";
|
||||||
qDebug() << "[PlaybackManager] mediaId:" << mediaId << "title:" << title;
|
qDebug() << "[PlaybackManager] mediaId:" << mediaId << "title:" << title;
|
||||||
qDebug() << "[PlaybackManager] streamUrl:" << LogRedactionUtils::url(streamUrl);
|
qDebug() << "[PlaybackManager] streamUrl:" << LogRedactionUtils::url(streamUrl);
|
||||||
|
|
||||||
|
|
||||||
bool independent = ConfigStore::instance()->get<bool>(ConfigKeys::PlayerIndependentWindow, false);
|
bool independent = ConfigStore::instance()->get<bool>(ConfigKeys::PlayerIndependentWindow, false);
|
||||||
if (independent) {
|
if (independent) {
|
||||||
qDebug() << "[PlaybackManager] → Mode: Independent window (internal)";
|
qDebug() << "[PlaybackManager] → Mode: Independent window (internal)";
|
||||||
launchIndependentWindow(mediaId, title, streamUrl, startPositionTicks, extraData);
|
launchIndependentWindow(mediaId, title, streamUrl, startPositionTicks, extraData);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!m_embeddedHostAvailable) {
|
||||||
|
qDebug() << "[PlaybackManager] → Mode: Independent window (internal, no embedded host)";
|
||||||
|
launchIndependentWindow(mediaId, title, streamUrl, startPositionTicks, extraData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
qDebug() << "[PlaybackManager] → Mode: Embedded player (internal)";
|
qDebug() << "[PlaybackManager] → Mode: Embedded player (internal)";
|
||||||
Q_EMIT requestEmbeddedPlay(mediaId, title, streamUrl, startPositionTicks, extraData);
|
Q_EMIT requestEmbeddedPlay(mediaId, title, streamUrl, startPositionTicks, extraData);
|
||||||
}
|
}
|
||||||
@@ -127,6 +142,35 @@ void PlaybackManager::stopExternalPlayer()
|
|||||||
cleanupExternalPlayerState();
|
cleanupExternalPlayerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PlaybackManager::stopActivePlaybackForQuit()
|
||||||
|
{
|
||||||
|
bool stopped = false;
|
||||||
|
|
||||||
|
// Independent QML player: close() deactivates PlayerViewModel (STOP report)
|
||||||
|
// then destroys the secondary engine. Host emits closed → playbackFinished.
|
||||||
|
if (m_independentHost) {
|
||||||
|
m_independentHost->close();
|
||||||
|
stopped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// External player process: kill it and flush the final stop report. This
|
||||||
|
// mirrors the process-exit handler's reportPlaybackStopped, but synchronously
|
||||||
|
// on the quit path where we cannot wait for the async exit signal.
|
||||||
|
if (m_extProcess && m_extProcess->state() != QProcess::NotRunning) {
|
||||||
|
long long finalTicks = m_ipc ? m_ipc->currentPositionTicks() : 0;
|
||||||
|
if (m_core && m_core->mediaService() && !m_currentMediaId.isEmpty() &&
|
||||||
|
!m_currentPlaySessionId.isEmpty()) {
|
||||||
|
m_core->mediaService()->reportPlaybackStopped(
|
||||||
|
m_currentMediaId, m_currentMediaSourceId, finalTicks,
|
||||||
|
m_currentPlaySessionId);
|
||||||
|
}
|
||||||
|
stopExternalPlayer();
|
||||||
|
stopped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stopped;
|
||||||
|
}
|
||||||
|
|
||||||
void PlaybackManager::cleanupExternalPlayerState()
|
void PlaybackManager::cleanupExternalPlayerState()
|
||||||
{
|
{
|
||||||
if (m_progressTimer) {
|
if (m_progressTimer) {
|
||||||
@@ -640,31 +684,16 @@ void PlaybackManager::launchIndependentWindow(const QString& mediaId, const QStr
|
|||||||
qDebug() << "[PlaybackManager] startPositionTicks:" << startPositionTicks
|
qDebug() << "[PlaybackManager] startPositionTicks:" << startPositionTicks
|
||||||
<< "(" << ticksToTimeString(startPositionTicks) << ")";
|
<< "(" << ticksToTimeString(startPositionTicks) << ")";
|
||||||
|
|
||||||
|
if (!m_independentHost) {
|
||||||
if (m_independentWindow) {
|
qDebug() << "[PlaybackManager] Creating IndependentPlayerHost";
|
||||||
qDebug() << "[PlaybackManager] Reusing existing independent window";
|
m_independentHost = new IndependentPlayerHost(m_core, this);
|
||||||
m_independentWindow->playMedia(mediaId, title, streamUrl, startPositionTicks, extraData);
|
connect(m_independentHost, &IndependentPlayerHost::closed, this, [this]() {
|
||||||
m_independentWindow->raise();
|
qDebug() << "[PlaybackManager] Independent player window closed";
|
||||||
m_independentWindow->activateWindow();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
qDebug() << "[PlaybackManager] Creating new independent window";
|
|
||||||
|
|
||||||
auto* playerWindow = new PlayerWindow(m_core);
|
|
||||||
playerWindow->setAttribute(Qt::WA_DeleteOnClose, true);
|
|
||||||
playerWindow->setMinimumSize(960, 540);
|
|
||||||
playerWindow->resize(1280, 720);
|
|
||||||
|
|
||||||
|
|
||||||
connect(playerWindow, &QObject::destroyed, this, [this]() {
|
|
||||||
m_independentWindow = nullptr;
|
|
||||||
qDebug() << "[PlaybackManager] Independent window destroyed";
|
|
||||||
Q_EMIT playbackFinished();
|
Q_EMIT playbackFinished();
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
qDebug() << "[PlaybackManager] Reusing IndependentPlayerHost";
|
||||||
|
}
|
||||||
|
|
||||||
m_independentWindow = playerWindow;
|
m_independentHost->play(mediaId, title, streamUrl, startPositionTicks, extraData);
|
||||||
|
|
||||||
playerWindow->show();
|
|
||||||
playerWindow->playMedia(mediaId, title, streamUrl, startPositionTicks, extraData);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user