Compare commits
15
Commits
a608910148
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ee3a234a6 | ||
|
|
e2357aaa32 | ||
|
|
a411fd396a | ||
|
|
b117bbf99e | ||
|
|
3002adb7bf | ||
|
|
ab1accc821 | ||
|
|
03bc33518c | ||
|
|
7979a108c9 | ||
|
|
4b650d4152 | ||
|
|
53ef34195e | ||
|
|
9a5e04d281 | ||
|
|
5717181e37 | ||
|
|
4fcfde8be3 | ||
|
|
cf58267d41 | ||
|
|
907de1feeb |
+7
-2
@@ -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)
|
||||||
@@ -87,7 +87,7 @@ set(QEMBY_QT_ROOT "" CACHE PATH
|
|||||||
qemby_append_default_qt_prefixes()
|
qemby_append_default_qt_prefixes()
|
||||||
|
|
||||||
# 寻找 Qt6 (不再允许降级)
|
# 寻找 Qt6 (不再允许降级)
|
||||||
find_package(Qt6 REQUIRED COMPONENTS Widgets Core Network Concurrent OpenGLWidgets LinguistTools WebSockets Svg)
|
find_package(Qt6 REQUIRED COMPONENTS Widgets Core Network Concurrent OpenGLWidgets LinguistTools WebSockets Svg Quick Qml QuickWidgets QuickControls2)
|
||||||
if(UNIX AND NOT APPLE)
|
if(UNIX AND NOT APPLE)
|
||||||
find_package(Qt6 QUIET COMPONENTS DBus)
|
find_package(Qt6 QUIET COMPONENTS DBus)
|
||||||
endif()
|
endif()
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# qEmby QML 迁移状态
|
||||||
|
|
||||||
|
更新时间:2026-07-27(设置缺口补全)
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
**唯一外壳 = 全 QML**(`AppShellHost` + `AppShell.qml`)。
|
||||||
|
Widgets 页面壳与播放器栈已删除。
|
||||||
|
**独立播放窗 = 第二个 QML `ApplicationWindow`**(`IndependentPlayerHost` + `IndependentPlayerWindow.qml`)。
|
||||||
|
|
||||||
|
保留 `QApplication`:托盘、文件对话框、业务 Widget 对话框(弹幕设置/识别、字幕设置、库/用户管理等)仍需要。
|
||||||
|
|
||||||
|
## 播放路径
|
||||||
|
|
||||||
|
| 模式 | 实现 |
|
||||||
|
|---|---|
|
||||||
|
| 壳内嵌入 | `PlayerRouteController` → 路由 `player` → `PlayerPage` |
|
||||||
|
| 独立窗口 | `IndependentPlayerHost` → 二级引擎 → `IndependentPlayerWindow` |
|
||||||
|
| 外部播放器 | `PlaybackManager` + `ExternalPlayerIPC` |
|
||||||
|
|
||||||
|
## 已删除(播放器栈 + 无引用 Widgets)
|
||||||
|
|
||||||
|
**播放器:** `PlayerWindow` `PlayerView` `NativeDanmakuOverlay` `MpvWidget` 及 OSD/切集/统计/长按/seek bubble 等。
|
||||||
|
|
||||||
|
**孤儿 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
|
||||||
|
|
||||||
|
| 角色 | 代表 |
|
||||||
|
|---|---|
|
||||||
|
| 播放对话框 | `PlayerDanmaku*Dialog` `PlayerSubtitleSettingsDialog` `PlayerOverlayDialog` |
|
||||||
|
| 管理/库 | `LibraryCreate/EditDlg` `UserEditDialog` `TaskEditDialog` `*Codec*Dialog` |
|
||||||
|
| 媒体操作 | `MediaActionMenu` `MediaIdentify/ImageEditDialog` `AddToPlaylistDialog` |
|
||||||
|
| 通用 | `ModernDialogBase` `ModernToast` `ModernMessageBox` `ProxySettingsDialog` `WebDav*` |
|
||||||
|
| 播放核心 | `MpvController` `PlayerDanmakuController` |
|
||||||
|
|
||||||
|
## 启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake --build cmake-build-debug --target qEmbyApp -j$(nproc)
|
||||||
|
./cmake-build-debug/bin/qEmbyApp
|
||||||
|
```
|
||||||
|
|
||||||
|
## 本轮设置补全
|
||||||
|
|
||||||
|
| 功能 | 位置 |
|
||||||
|
|---|---|
|
||||||
|
| 主页区块自定义排序(↑↓ + 重置) | `SettingsLibraryPage` + `SettingsViewModel::{customHomeSectionOrderEnabled,homeSectionOrder,moveHomeSection}` |
|
||||||
|
| 弹幕 API 服务器管理(选/启停/增删改) | `SettingsPlayerPage` + `DanmakuSettings` 桥接 |
|
||||||
|
| 缺失弹幕属性 | `danmakuSourceMode` / `AutoLoad` / `AutoMatch` / `WithRelated` / `CacheHours` |
|
||||||
|
| 库开关按服务器作用域 | `showContinueWatching` 等写入 `server/<id>/library/...`(与 Dashboard 一致,兼容读全局旧值) |
|
||||||
|
|
||||||
|
## 本轮优化(2026-07-27)
|
||||||
|
|
||||||
|
| 项 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| 设置缓存清理 | `clearRecommendCache` / `clearImageCache` 已接线 + toast |
|
||||||
|
| 代理设置 | 设置-通用「配置…」打开 `ProxySettingsDialog` |
|
||||||
|
| 列表性能 | MediaCard DPR sourceSize、滑动关 smooth/mipmap;Grid/Section 减小 cacheBuffer |
|
||||||
|
| 首页 | 区块 Loader 异步;reduceMotion 跳过入场动画 |
|
||||||
|
| 路由 | reduceMotion / 播放器页瞬时切页,少缩放动画 |
|
||||||
|
|
||||||
|
## 后续可选
|
||||||
|
|
||||||
|
- 播放器内弹幕设置 / 识别 / 字幕对话框 QML 化
|
||||||
|
- 主页排序改为拖拽(当前为上下按钮,足够用)
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
# QML 导航契约与兼容迁移规格
|
||||||
|
|
||||||
|
状态:Draft(Phase 0/1 兼容基础已实现,尚未形成已接受 ADR)
|
||||||
|
更新时间:2026-07-15
|
||||||
|
范围:应用登录/主页切换、`HomeView` 业务页面栈、设置/管理子页面导航,以及迁移到 QML 时的最小 C++/QML 边界。
|
||||||
|
非目标:本规格不决定播放器渲染技术、不迁移任何页面、不替换平台窗口/托盘代码,也不规定业务 ViewModel 的数据接口。
|
||||||
|
|
||||||
|
## 1. 证据口径
|
||||||
|
|
||||||
|
本文严格区分:
|
||||||
|
|
||||||
|
- **Observed fact(已观察事实)**:直接来自当前源码。
|
||||||
|
- **Proposed decision(建议决策)**:为 QML 迁移定义的目标契约,只有在评审接受后才是实现约束。
|
||||||
|
- **Needs confirmation(待确认)**:产品或架构选择,当前源码不足以给出唯一答案。
|
||||||
|
|
||||||
|
主要证据文件:
|
||||||
|
|
||||||
|
- `src/qEmbyApp/mainwindow.{h,cpp}`
|
||||||
|
- `src/qEmbyApp/views/user/homeview.{h,cpp}`
|
||||||
|
- `src/qEmbyApp/views/baseview.{h,cpp}`
|
||||||
|
- `src/qEmbyApp/views/settings/settingsview.cpp`
|
||||||
|
- `src/qEmbyApp/views/admin/manageview.cpp`
|
||||||
|
- `src/qEmbyApp/quick/quickpagehost.{h,cpp}`
|
||||||
|
|
||||||
|
## 2. 当前导航模型(Observed facts)
|
||||||
|
|
||||||
|
### 2.1 两层页面切换
|
||||||
|
|
||||||
|
1. `MainWindow` 持有 `QStackedWidget m_viewStack`,其中长期存在一个 `LoginView` 和一个 `HomeView`。
|
||||||
|
2. `LoginView::loginCompleted` 调用 `MainWindow::navigateToHome()`;`HomeView::logoutRequested` 调用 `MainWindow::navigateToLogin()`。
|
||||||
|
3. 登录/主页之间使用淡入动画;配置 `UiAnimations` 命中的现有分支会直接切换。
|
||||||
|
4. `navigateToLogin()` 在切到登录页后调用 `AuthService::logout()`。退出应用也会调用该方法。
|
||||||
|
5. `HomeView` 内部使用 `SlidingStackedWidget m_contentSwitcher` 和 `QStack<RouteInfo> m_navStack` 管业务页面。
|
||||||
|
6. `DashboardView` 与 `FavoritesView` 在 `HomeView::setupUi()` 中创建并长期保留;当前没有给这两个 QWidget 设置 `routeType/routeId/routeTitle/isDynamic`。
|
||||||
|
|
||||||
|
### 2.2 当前 RouteInfo 与动态属性
|
||||||
|
|
||||||
|
`RouteInfo` 当前字段为:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
QPointer<QWidget> widget;
|
||||||
|
bool isDynamic;
|
||||||
|
QString routeType;
|
||||||
|
QString routeId;
|
||||||
|
QString routeTitle;
|
||||||
|
QString routeExtraId;
|
||||||
|
```
|
||||||
|
|
||||||
|
`pushView()` 在切换前从当前 QWidget 的同名动态属性复制元数据到历史栈。页面实例可能被删除,因此返回逻辑也依赖这些字符串重建页面。
|
||||||
|
|
||||||
|
| create/静态实例 | 实际 QWidget | `isDynamic` | `routeType` | `routeId` | `routeTitle` | `routeExtraId` | 初始化事实 |
|
||||||
|
|---|---|---:|---|---|---|---|---|
|
||||||
|
| `m_dashboardView` | `DashboardView` | 未设置/false | 未设置 | 未设置 | 未设置 | 未设置 | 构造时创建,`goHome()` 复用;已在首页时再次调用会刷新数据 |
|
||||||
|
| `m_favoritesView` | `FavoritesView` | 未设置/false | 未设置 | 未设置 | 未设置 | 未设置 | 构造时创建,`goFav()` 复用;已在收藏时再次调用会刷新数据 |
|
||||||
|
| `createDetailView(itemId,itemName,seedItem)` | `DetailView` | true | `DetailView` | `itemId` | `itemName` | 未设置 | `loadItem(itemId, seedItem)`;历史重建时没有 `seedItem` |
|
||||||
|
| `createCategoryView(categoryId,title)` | `CategoryView` | true | `CategoryView` | `categoryId` | `title` | 未设置 | 异步排队调用 `loadCategory(categoryId,title)`;收藏分类入口给 id 加 `Favorite_` 前缀 |
|
||||||
|
| `createLibraryView(libraryId,title)` | `LibraryView` | true | `LibraryView` | `libraryId` | `title` | 未设置 | `loadLibrary(libraryId,title)` |
|
||||||
|
| `createPersonView(personId,personName)` | `LibraryView` | true | `PersonView` | `personId` | `personName` | 未设置 | 复用 `LibraryView`,调用 `loadPerson(personId,personName)` |
|
||||||
|
| `createSearchView(query)` | `SearchView` | true | `SearchView` | `query` | 未设置 | 未设置 | `performSearch(query)` |
|
||||||
|
| `createFilteredView(filterType,filterValue)` | `LibraryView` | true | `FilteredView` | `filterType + ":" + filterValue` | `filterValue` | `filterType` | 复用 `LibraryView`,调用 `loadFiltered(filterType,filterValue)` |
|
||||||
|
| `createSeasonView(seriesId,seasonId,seasonName)` | `SeasonView` | true | `SeasonView` | `seasonId` | `seasonName` | `seriesId` | `loadSeason(seriesId,seasonId,seasonName)` |
|
||||||
|
| `createPlayerView(mediaId,title,streamUrl,ticks,extraData)` | `PlayerView` | true | `PlayerView` | `mediaId` | `title` | 未设置 | 等切页动画结束后 `playMedia(...)`;进入/退出播放器采用瞬切 |
|
||||||
|
| `createSettingsView()` | `SettingsView` | true | `SettingsView` | `settings_global` | 翻译后的 Settings | 未设置 | 侧栏仅避免连续打开当前 `SettingsView` |
|
||||||
|
| `createManageView()` | `ManageView` | true | `ManageView` | `manage_global` | 翻译后的 Server Management | 未设置 | 侧栏仅避免连续打开当前 `ManageView` |
|
||||||
|
|
||||||
|
注意:当前 `navigateBack()` 能按元数据重建 Detail、Library、Category、Search、Settings、Person、Season、Manage、Filtered;没有 `PlayerView` 重建分支。找不到实例且无法重建时回退到 Dashboard。
|
||||||
|
|
||||||
|
### 2.3 create 方法和导航信号的事实映射
|
||||||
|
|
||||||
|
`BaseView` 暴露以下导航信号:
|
||||||
|
|
||||||
|
| 现有信号 | 参数 | `HomeView` 中的实际目标 |
|
||||||
|
|---|---|---|
|
||||||
|
| `navigateToDetail` | `itemId,itemName,seedItem` | `pushView(createDetailView(...))` |
|
||||||
|
| `navigateToFolder` | `folderId,folderName` | `pushView(createLibraryView(...))` |
|
||||||
|
| `navigateToPerson` | `personId,personName` | `pushView(createPersonView(...))` |
|
||||||
|
| `triggerSearch` | `query` | trim、记录服务器维度搜索历史、`pushView(createSearchView(query))` |
|
||||||
|
| `navigateToFilteredView` | `filterType,filterValue` | `pushView(createFilteredView(...))` |
|
||||||
|
| `navigateBack` | 无 | `HomeView::navigateBack()`(只在部分页面显式连接) |
|
||||||
|
| `navigateToCategory` | `categoryId,title` | Dashboard/Favorites 的专用连接进入 Category;Favorites 会加 `Favorite_` 前缀 |
|
||||||
|
| `navigateToPlayer` | `mediaId,title,streamUrl,ticks,extraData` | `launchPlayer()` → `PlaybackManager::startPlayback()`;嵌入播放请求最终 `pushView(createPlayerView(...))` |
|
||||||
|
| `navigateToSeason` | `seriesId,seasonId,seasonName` | `pushView(createSeasonView(...))` |
|
||||||
|
|
||||||
|
页面连接并不完全一致:Detail 额外连接过滤和搜索;Season 只连接详情、播放和返回;Search/Category/Library 连接其各自支持的详情/文件夹/人物/播放/季入口。目标导航控制器不应依赖每个页面重复手写这些连接。
|
||||||
|
|
||||||
|
### 2.4 push/back/home 的现有生命周期
|
||||||
|
|
||||||
|
- `pushView(view)`:把当前页面压栈;把目标加入 switcher;排队调用目标的 `scrollToTop()`;非 Player 向左滑入,Player 瞬切。
|
||||||
|
- 活跃历史中最多保留 12 个仍有 QWidget 指针的动态页面。超过限制的旧动态页面从 switcher 移除并 `deleteLater()`,但历史元数据仍保留以便重建。
|
||||||
|
- `navigateBack()`:先让当前 `BaseView::handleBackNavigation()` 尝试消费返回;未消费时调用 `prepareForStackLeave()`,弹出历史;必要时重建;完成切换后销毁离开的动态页。
|
||||||
|
- `resetToView(Dashboard/Favorites)`:清空整个历史栈、调用当前页 `prepareForStackLeave()`、滚动目标到顶并销毁相关动态页面。
|
||||||
|
- `goHome()/goFav()` 在已经位于目标静态页面时刷新,而不是产生新历史。
|
||||||
|
- `MainWindow` 返回按钮顺序是:业务栈 back → 回 Dashboard → 2 秒内再次返回触发退出登录提示/动作。
|
||||||
|
- `BaseView::scrollToTop()` 默认为空;当前检索到 Dashboard 和 Detail 有 override。没有通用的滚动位置序列化/恢复契约。
|
||||||
|
- `PlayerView` override `prepareForStackLeave()`;Manage 的 show/hide 连接/断开 WebSocket;离开 Manage 后 `HomeView` 还会刷新侧栏媒体库。
|
||||||
|
|
||||||
|
### 2.5 设置与管理子导航
|
||||||
|
|
||||||
|
- 设置是一个全局 `SettingsView` route,内部 5 个索引依次是 `general`、`appearance`、`library`、`player`、`about`。页面惰性实例化;当前外层 route 不记录选中索引或滚动状态。
|
||||||
|
- 管理是一个全局 `ManageView` route,内部 7 个索引依次是 `dashboard`、`libraries`、`collections/lists`、`transcoding`、`users`、`tasks`、`logs`。当前外层 route 不记录选中索引或滚动状态。
|
||||||
|
- 设置五个页面主体已由 `QuickPageHost` 加载 QML;管理仅 Dashboard 主体由该 host 加载。两者的左侧导航和父级页面栈仍是 Widgets。
|
||||||
|
- `QuickPageHost` 通过独立 `QQuickWidget` 加载 URL、注入 context property/image provider,并把 QWidget show/hide 转成 `pageShown/pageHidden`。
|
||||||
|
|
||||||
|
## 3. 目标职责边界(Proposed decision)
|
||||||
|
|
||||||
|
采用一个 C++ `NavigationController` 作为**唯一规范导航状态所有者**,QML `Router.qml` 作为声明式页面注册、实例化和视觉切换层:
|
||||||
|
|
||||||
|
```text
|
||||||
|
QML 页面/侧栏/标题栏
|
||||||
|
│ push/replace/back/home
|
||||||
|
▼
|
||||||
|
NavigationController (C++:校验、权限、规范栈、会话清理)
|
||||||
|
│ currentRoute/transition request
|
||||||
|
▼
|
||||||
|
Router.qml (QML:route→Component、Loader/StackView、过渡、UI state 回写)
|
||||||
|
│
|
||||||
|
├─ QML Page + C++ ViewModel
|
||||||
|
└─ 迁移期 LegacyWidgetRouteHost
|
||||||
|
```
|
||||||
|
|
||||||
|
约束:
|
||||||
|
|
||||||
|
1. 页面不得自行维护第二份全局历史,也不得直接实例化另一个顶级页面。
|
||||||
|
2. C++ Controller 不持有 QML `Item*` 作为历史真相;历史必须只靠可验证 route 数据恢复。
|
||||||
|
3. QML Router 不做认证、管理员权限或 deep-link 参数校验;这些属于 Controller。
|
||||||
|
4. 业务加载/错误属于页面 ViewModel;“未知 route、参数非法、权限拒绝、无法实例化”属于导航错误。
|
||||||
|
5. 迁移期间 Widgets 和 QML 入口都调用同一 Controller,旧 `HomeView` 栈仅作为尚未迁移页面的适配实现。
|
||||||
|
|
||||||
|
## 4. Route shape(Proposed decision)
|
||||||
|
|
||||||
|
C++/QML 边界使用 `QVariantMap`,内部 C++ 应使用强类型 `Route` 后再导出,避免核心逻辑依赖无类型 map。
|
||||||
|
|
||||||
|
```qml
|
||||||
|
{
|
||||||
|
name: "detail", // 稳定、非翻译的 route 名
|
||||||
|
key: "detail:item-123", // Controller 规范化生成的实例标识
|
||||||
|
title: "Movie title", // 展示提示;不得作为重建必需参数
|
||||||
|
params: { itemId: "item-123" },
|
||||||
|
presentation: "page", // page | immersive
|
||||||
|
cachePolicy: "lru", // singleton | lru | destroyOnPop
|
||||||
|
uiState: { scrollY: 0 }, // 可丢失的页面表现状态
|
||||||
|
version: 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
规则:
|
||||||
|
|
||||||
|
- `name + params` 是恢复页面的规范输入;`title` 只用于即时展示。
|
||||||
|
- `key` 由 Controller 根据 route 注册表生成,调用方不能伪造。
|
||||||
|
- `params` 只允许 JSON/QVariant 可安全复制的标量、list、map;不得把 `QObject*`、QWidget 指针、原始 `MediaItem` 或带凭据的流 URL写入历史。
|
||||||
|
- `uiState` 可更新、可淘汰,不参与 route 身份;第一阶段至少支持 `scrollY` 和嵌套页面 `section`。
|
||||||
|
- 播放的 `streamUrl/extraData` 目前可能包含瞬态或复杂数据。它们保留在 PlaybackManager 的启动上下文,不进入可持久化 deep link;Player route 历史只保存 `mediaId/title/startPositionTicks`,且默认 `destroyOnPop`。
|
||||||
|
- `seedItem` 是详情加载优化,不是恢复必需数据;可作为一次性 Controller payload 传递,但不能成为 route schema 的必填项。
|
||||||
|
|
||||||
|
### 4.1 规范 route 注册表
|
||||||
|
|
||||||
|
| `name` | 必填 `params` | 可选 `params` | 兼容来源 | 默认缓存 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `login` | 无 | 无 | MainWindow LoginView | singleton(应用级) |
|
||||||
|
| `home` | 无 | 无 | DashboardView | singleton(会话级) |
|
||||||
|
| `favorites` | 无 | 无 | FavoritesView | singleton(会话级) |
|
||||||
|
| `detail` | `itemId` | `title` | DetailView | lru |
|
||||||
|
| `category` | `categoryId` | `title`, `favorite` | CategoryView;兼容期解析 `Favorite_` 前缀 | lru |
|
||||||
|
| `library` | `libraryId` | `title` | LibraryView | lru |
|
||||||
|
| `person` | `personId` | `title` | PersonView/LibraryView | lru |
|
||||||
|
| `search` | `query` | 无 | SearchView | lru |
|
||||||
|
| `filtered` | `filterType`,`filterValue` | `title` | FilteredView | lru |
|
||||||
|
| `season` | `seriesId`,`seasonId` | `title` | SeasonView | lru |
|
||||||
|
| `player` | `mediaId` | `title`,`startPositionTicks` | PlayerView | destroyOnPop |
|
||||||
|
| `settings` | 无 | `section` | SettingsView | lru |
|
||||||
|
| `manage` | 无 | `section` | ManageView | lru;admin guard |
|
||||||
|
| `navigationError` | `code` | `message`,`failedRoute` | 当前无等价页 | destroyOnPop |
|
||||||
|
|
||||||
|
Settings `section` allowlist:`general|appearance|library|player|about`。
|
||||||
|
Manage `section` allowlist:`dashboard|libraries|collections|transcoding|users|tasks|logs`。
|
||||||
|
|
||||||
|
**Needs confirmation:** `collections` 是否作为面向用户的稳定 deep-link 名,还是沿用界面文字 `lists`;建议内部稳定名使用 `collections`。
|
||||||
|
|
||||||
|
## 5. NavigationController 最小契约(Proposed decision)
|
||||||
|
|
||||||
|
以下是语义契约,不强制具体头文件拼写:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class NavigationController : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(QVariantMap currentRoute READ currentRoute NOTIFY currentRouteChanged)
|
||||||
|
Q_PROPERTY(bool canGoBack READ canGoBack NOTIFY stackChanged)
|
||||||
|
Q_PROPERTY(int depth READ depth NOTIFY stackChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
Q_INVOKABLE bool push(const QVariantMap &request);
|
||||||
|
Q_INVOKABLE bool replace(const QVariantMap &request);
|
||||||
|
Q_INVOKABLE bool back();
|
||||||
|
Q_INVOKABLE void home();
|
||||||
|
Q_INVOKABLE void updateUiState(const QVariantMap &patch);
|
||||||
|
|
||||||
|
// 仅认证/会话协调层调用,不暴露给任意内容页。
|
||||||
|
void enterAuthenticatedSession();
|
||||||
|
void leaveAuthenticatedSession(LogoutReason reason);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void currentRouteChanged();
|
||||||
|
void stackChanged();
|
||||||
|
void navigationRejected(QString code, QString message,
|
||||||
|
QVariantMap request);
|
||||||
|
void sessionRoutesCleared();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
操作语义:
|
||||||
|
|
||||||
|
- `push`:校验并规范化 request;保存当前 `uiState`;压入新 route。新页面 `scrollY=0`。默认允许相同 route 重复入栈。
|
||||||
|
- `replace`:校验成功后用新 route 替换栈顶,不保留被替换项;适用于登录完成、参数规范化和不可返回的重定向。
|
||||||
|
- `back`:先请求当前页面处理局部返回(关闭 overlay、退出子模式);未消费才弹全局栈。目标恢复已保存 `uiState`。
|
||||||
|
- `home`:清空业务历史并切到 `home`;已经在 `home` 时发出 `refreshRequested(key)`,对齐现有刷新语义。
|
||||||
|
- 收藏入口提供等价的 `resetTo({name:"favorites"})` 内部 API;它和 `home()` 一样清业务栈。若不希望扩大公开 API,可由 `replace` 的受控 `clearHistory` option 实现,但普通页面不能传此 option。
|
||||||
|
- `updateUiState`:只合并 allowlist 字段,节流写入当前栈项;不产生页面切换。
|
||||||
|
- 所有失败操作都必须原子化:返回 false、栈不变、发 `navigationRejected`。
|
||||||
|
|
||||||
|
**Needs confirmation:** 当前“点收藏清栈”的行为是否继续保留。建议保留以匹配 `resetToView(m_favoritesView)`,并将 `favorites` 与 `home` 都视为会话根页面。
|
||||||
|
|
||||||
|
### 5.1 局部返回与离开钩子
|
||||||
|
|
||||||
|
为兼容 `BaseView::handleBackNavigation()` 和 `prepareForStackLeave()`,QML 页面统一可选实现:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
function handleBackRequest() { return false }
|
||||||
|
function prepareForRouteLeave(reason) { }
|
||||||
|
function captureRouteUiState() { return { scrollY: flickable.contentY } }
|
||||||
|
function restoreRouteUiState(state) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
Router 在 Controller `back/home/replace/sessionClear` 提交前调用前两个钩子。`handleBackRequest()` 返回 true 时,不调用 Controller `back()`。异步清理不能无限阻塞导航;耗时业务取消应由 ViewModel 的 active/generation/cancellation 机制完成。
|
||||||
|
|
||||||
|
## 6. QML Router 最小职责(Proposed decision)
|
||||||
|
|
||||||
|
1. 维护静态 route name → `Component` 注册表,不根据任意 URL 拼 QML 文件路径。
|
||||||
|
2. 监听 `currentRoute` 并实例化对应组件;把 `params`、一次性 payload、Controller/ViewModel 注入页面。
|
||||||
|
3. 根据 `cachePolicy` 执行:
|
||||||
|
- `singleton`:会话内唯一实例,离开时停用但不销毁;
|
||||||
|
- `lru`:最多 12 个**活页面实例**,与现有上限一致;被淘汰后保留 route/uiState,返回时重建;
|
||||||
|
- `destroyOnPop`:离开即销毁,Player 默认使用。
|
||||||
|
4. 页面 `visible`/active 变化必须传给 ViewModel,以替代 `QuickPageHost::pageShown/pageHidden`,停止轮询、WebSocket 或过期请求。
|
||||||
|
5. Fresh push 从顶部开始;back 恢复保存的滚动位置;home/favorites reset 后目标滚到顶部。滚动恢复要在模型/布局可用后 clamp 到合法范围。
|
||||||
|
6. Player 使用 immersive presentation 并禁用普通滑页动画;其余页面由统一 transition policy 决定,reduce-motion 时无动画。
|
||||||
|
7. 实例创建或 Component error 时报告 Controller,并展示 `navigationError` 或安全回退;禁止黑屏或静默回 home。
|
||||||
|
|
||||||
|
## 7. 深链与参数(Proposed decision)
|
||||||
|
|
||||||
|
第一阶段先支持内部 deep link(规范 route map),外部 URI 注册推迟到纯 QML 壳稳定后。建议 URI 形态:
|
||||||
|
|
||||||
|
```text
|
||||||
|
qemby://detail/<itemId>
|
||||||
|
qemby://library/<libraryId>
|
||||||
|
qemby://season/<seriesId>/<seasonId>
|
||||||
|
qemby://settings/<section>
|
||||||
|
qemby://manage/<section>
|
||||||
|
qemby://search?q=<percent-encoded-query>
|
||||||
|
```
|
||||||
|
|
||||||
|
处理规则:
|
||||||
|
|
||||||
|
- Controller 维护 route/参数 allowlist、长度限制和 percent-decoding;未知字段忽略还是拒绝必须逐 route 固定,建议拒绝必填字段冲突、忽略未知可选字段并记录日志。
|
||||||
|
- 未登录时收到业务 deep link:保存**一个经过校验的 pending route**,切到 login;登录成功后 replace 为 home,再 push pending route。
|
||||||
|
- `manage` 在未登录、非管理员或服务器不支持时拒绝,不实例化管理页。
|
||||||
|
- server-scoped item deep link 必须绑定当前 server;跨服务器解析/选择流程当前无事实依据。
|
||||||
|
- 不允许在 URI、route 或日志中携带 access token、密码、完整 stream URL。
|
||||||
|
|
||||||
|
**Needs confirmation:** 是否支持 OS 外部 deep link、跨服务器 deep link,以及登录后 pending link 的过期时间。建议首期只保证应用内 route map,外部 URI 单独立项。
|
||||||
|
|
||||||
|
## 8. 登录、退出与服务器切换(Proposed decision)
|
||||||
|
|
||||||
|
### 登录成功
|
||||||
|
|
||||||
|
1. Controller 建立新的 session generation。
|
||||||
|
2. 清除任何上一个会话的页面实例、历史和 uiState。
|
||||||
|
3. `replace(home)`,不允许 back 回 login。
|
||||||
|
4. 若存在合法 pending deep link,再 `push` 目标。
|
||||||
|
|
||||||
|
### 退出登录
|
||||||
|
|
||||||
|
1. 先让 Player/当前页执行 leave hook,并停止会话级任务。
|
||||||
|
2. 原子清空业务栈、LRU cache、一次性 payload、pending deep link、滚动状态和所有 session singleton。
|
||||||
|
3. 断开 Manage WebSocket/页面轮询。
|
||||||
|
4. 调用认证服务 logout,然后 `replace(login)`;不允许 back 回任何业务页。
|
||||||
|
|
||||||
|
### 切换服务器/用户
|
||||||
|
|
||||||
|
按“退出旧 session + 建立新 session”处理,不能复用旧 server 的 route 页面或滚动状态。搜索历史本身当前按 server id 隔离,这不代表页面栈可跨 server 复用。
|
||||||
|
|
||||||
|
当前 Widgets 行为在侧栏 logout 时会先 reset Dashboard,但 `MainWindow::navigateToLogin()` 本身不直接拥有 `HomeView` 清栈 API。目标契约必须把清栈变成 Controller 的会话不变量,而不是依赖退出入口恰好来自侧栏。
|
||||||
|
|
||||||
|
## 9. 设置与管理页(Proposed decision)
|
||||||
|
|
||||||
|
- 顶层保持一个 `settings` 和一个 `manage` route,不把每个 section 都变成全局历史项。
|
||||||
|
- `params.section` 支持侧栏/深链直达;同一页面内切 section 使用 `replace` 更新当前 route 参数,因此 back 返回离开 Settings/Manage 前的业务页面,而不是逐 tab 返回。
|
||||||
|
- section 自己保存独立 `scrollY`,建议 `uiState.sections[section].scrollY`;LRU 重建后恢复所选 section 和滚动。
|
||||||
|
- Manage route 必须由 Controller 做 admin guard;页面仍应处理服务端 401/403,因为登录后权限可能变化。
|
||||||
|
- 离开 Manage(包括 back/home/logout/server switch)必须停 WebSocket;进入时才连接。迁移期继续触发当前“离开管理后刷新侧栏媒体库”的兼容动作。
|
||||||
|
- 设置/管理内部 QML 页面不应各自创建 `QQuickWidget`。壳迁移后在同一 QML engine 中用 Component/Loader 承载;`QuickPageHost` 只保留给尚未迁移的 Widgets 壳阶段。
|
||||||
|
|
||||||
|
**Needs confirmation:** 切换设置/管理 section 是否应该进入浏览器式 back 历史。建议不进入,以保持当前全局栈行为。
|
||||||
|
|
||||||
|
## 10. 错误处理(Proposed decision)
|
||||||
|
|
||||||
|
| 错误 | Controller/Router 行为 | 用户可恢复动作 |
|
||||||
|
|---|---|---|
|
||||||
|
| 未知 route name | 拒绝、栈不变、日志 + `navigationRejected(unknownRoute)` | 保持当前页 |
|
||||||
|
| 缺失/非法参数 | 拒绝、栈不变,不启动 ViewModel 请求 | 修正入口;外部 link 显示不可打开提示 |
|
||||||
|
| 未认证 | 保存合法 pending route,replace login | 登录或取消 |
|
||||||
|
| 非管理员访问 manage | 拒绝;不得短暂显示管理内容 | 返回当前页/提示权限不足 |
|
||||||
|
| QML Component 加载失败 | 标记 `componentLoadFailed`,展示 navigationError;保留可 back/home 的壳 | 重试、back、home |
|
||||||
|
| 页面数据请求失败 | route 保持不变,由页面 ErrorState/ViewModel 处理 | 重试、back |
|
||||||
|
| LRU 页面已销毁 | 由 route params 重建并恢复可用 uiState | 无需用户干预 |
|
||||||
|
| 历史数据版本不兼容 | 丢弃该项并继续向前找安全 route;记录原因 | 最终回 home/login |
|
||||||
|
| leave hook/动画期间重复导航 | Controller 串行化或合并;不能出现双 pop/重复实例 | 操作完成后状态一致 |
|
||||||
|
|
||||||
|
不得使用“任何错误都静默回 Dashboard”的策略;现有 fallback 只作为兼容期最后保险。
|
||||||
|
|
||||||
|
## 11. 分阶段兼容方案(Proposed decision)
|
||||||
|
|
||||||
|
### Phase 0:固定契约与测试夹具
|
||||||
|
|
||||||
|
- 新增强类型 Route/注册表和 Controller 单元测试,不改变实际导航入口。
|
||||||
|
- 建立 legacy 属性 ↔ route 的双向转换;记录不完整历史项但仍走现有 HomeView。
|
||||||
|
- 为 login/home/favorites 补稳定 route 名(可先只在适配器中表达,不要求立刻改 QWidget 属性)。
|
||||||
|
|
||||||
|
### Phase 1:Controller 旁路接管
|
||||||
|
|
||||||
|
- 所有 Widgets 信号先转换成 route request,再由 legacy adapter 调用现有 `create*View/pushView/resetToView`。
|
||||||
|
- 对比 Controller 栈与 `m_navStack`,Debug 下发现漂移立即记录。
|
||||||
|
- 会话退出、服务器切换首先统一走 Controller clear-session 流程。
|
||||||
|
|
||||||
|
### Phase 2:单引擎 QML Router + 已迁移页面
|
||||||
|
|
||||||
|
- 在现有 Widgets MainWindow/HomeView 内放置**一个**根 Quick host,Router 接管 Login、Search、Settings 主体、Manage Dashboard 等已迁移页面。
|
||||||
|
- 未迁移 route 通过 `LegacyWidgetRouteHost`/明确的边界回到 Widgets;不要为每个 QML 子组件新增独立 QQuickWidget。
|
||||||
|
- Controller 成为唯一历史;停止让 QML 路径同时写 `m_navStack`。
|
||||||
|
|
||||||
|
### Phase 3:QML Home shell
|
||||||
|
|
||||||
|
- 迁移侧栏、全局搜索、标题导航和内容 StackView;保留 C++ 原生窗口 agent、托盘及必要平台能力。
|
||||||
|
- Dashboard/Favorites/Category/Library/Detail/Season 逐个切 route factory;每个入口保留 feature flag 回退一个发布周期。
|
||||||
|
- 设置/管理 section 和滚动状态切到本文契约。
|
||||||
|
|
||||||
|
### Phase 4:移除 legacy 业务栈
|
||||||
|
|
||||||
|
- 最后迁移 Player/长尾页面或明确其原生 host 边界。
|
||||||
|
- 删除 `HomeView::create*View()`、`RouteInfo`、重复 signal wiring 和 `QuickPageHost` 多实例路径前,必须完成全链路验收。
|
||||||
|
- 平台窗口、托盘、文件选择器等 C++ 能力无需为了“纯 QML”被删除。
|
||||||
|
|
||||||
|
## 12. 验收标准
|
||||||
|
|
||||||
|
### 契约与自动测试
|
||||||
|
|
||||||
|
- 每个注册 route 的合法/缺参/错参用例都有 Controller 测试;失败时栈严格不变。
|
||||||
|
- push → push → back、replace、home/favorites reset、空栈 back 均有确定结果。
|
||||||
|
- LRU 超过 12 个实例后,历史 route 仍能重建;Player 不被重建为带失效 stream context 的页面。
|
||||||
|
- uiState 不参与 key;fresh push 到顶,back 恢复滚动,超出新 contentHeight 时正确 clamp。
|
||||||
|
- logout、切 server/user 后 depth、cache、pending payload、scroll state 均不含旧 session 数据。
|
||||||
|
- 未登录 deep link、非法 deep link、非管理员 manage deep link 有测试。
|
||||||
|
|
||||||
|
### 兼容行为
|
||||||
|
|
||||||
|
- Dashboard、Favorites、Detail、Category、Library、Person、Search、Filtered、Season、Settings、Manage、Player 的所有现有入口都有 route 映射。
|
||||||
|
- `BaseView` 九类导航信号在兼容适配器中无遗漏;同一入口不会同时 push 两个栈。
|
||||||
|
- 返回优先关闭当前页面局部状态,再 pop;离开 Player 执行 stop/report 语义,离开 Manage 停 WebSocket。
|
||||||
|
- 标题栏 back/home/favorites、侧栏入口、全局搜索、登录成功、侧栏退出、双击返回退出都到达同一 Controller API。
|
||||||
|
- Settings/Manage 直达 section、返回到来源页、重建后恢复 section/滚动均可用。
|
||||||
|
|
||||||
|
### 运行与视觉
|
||||||
|
|
||||||
|
- `qEmbyApp_qmllint` 与 Debug 构建通过,无新增 runtime QML error/warning。
|
||||||
|
- 登录 → Home → Library → Detail → Season/Player → back → Settings → Manage → logout 完整链路重复 10 次无崩溃、无幽灵页面、无跨账号数据。
|
||||||
|
- reduce-motion 与普通动画两种配置下快速连续 back/push 不产生双 pop。
|
||||||
|
- Emby/Jellyfin、管理员/普通用户分别冒烟;窗口缩放、键盘 Back/Escape、鼠标侧键、托盘恢复后导航状态一致。
|
||||||
|
- QML Component 故障和网络错误分别验证:前者进入导航错误 UI,后者留在当前业务 route 的错误态。
|
||||||
|
|
||||||
|
## 13. 待评审决定汇总
|
||||||
|
|
||||||
|
1. Favorites 是否继续作为会话根并在进入时清空历史(建议:是,兼容现状)。
|
||||||
|
2. Settings/Manage section 切换是否写入全局 back 历史(建议:否)。
|
||||||
|
3. 管理合集 section 的稳定名使用 `collections` 还是 `lists`(建议:`collections`)。
|
||||||
|
4. 首期是否包含 OS 外部 deep link 和跨服务器解析(建议:不包含,只做内部 route map)。
|
||||||
|
5. pending deep link 的过期策略(建议:只存一个、仅当前登录流程有效,取消登录即清除)。
|
||||||
|
6. QML 壳过渡期采用何种 Legacy QWidget 承载技术,需要在 Player/原生复杂页原型后单独决策;本规格只要求单一规范栈,未把具体承载方案当作事实。
|
||||||
|
|
||||||
|
## 14. 实施记录
|
||||||
|
|
||||||
|
2026-07-15 完成 Phase 0/1 的首个兼容批次:
|
||||||
|
|
||||||
|
- 新增 `NavigationRoute` 与 `NavigationController`,支持规范 key、route 参数校验、认证/管理员门禁、push/replace/back、Home/Favorites reset、会话清理和 UI state 白名单。
|
||||||
|
- `HomeView` 将现有 legacy 动态属性统一转换为 route request;控制器校验成功后才操作 Widgets 页面栈。旧 `m_navStack` 暂时保留用于页面实例和动画兼容。
|
||||||
|
- `MainWindow` 的登录/退出切换已通知控制器建立或清理认证会话。
|
||||||
|
- 采用建议默认值:Favorites 是会话根;Settings/Manage section 不写全局历史;管理合集稳定名为 `collections`;当前不实现 OS/跨服务器 deep link。
|
||||||
|
- 新增 `qEmbyNavigationTest`,覆盖原子拒绝、栈操作、权限、UI state 与 logout 清理。下一批实现单引擎 `Router.qml`,届时控制器才从“旁路规范状态”升级为页面实例切换的直接驱动者。
|
||||||
|
|
||||||
|
2026-07-15 完成 Phase 2 首批 Home/Favorites:
|
||||||
|
|
||||||
|
- 新增应用级 `QuickRouterHost` 与 `Router.qml`;首批只静态注册 Home/Favorites,其他 route 不解析未注入的页面类型,也不会在 legacy Widgets 滑入期间闪现恢复页。
|
||||||
|
- 新增 `HomeQuickRouterView`,在同一 engine 中注入 Dashboard/Favorites 两个会话单例 ViewModel,并按规范 route 显式切换 active 生命周期。
|
||||||
|
- 播放、收藏、详情/媒体库/人物/分类导航、通用菜单及 Dashboard 管理媒体库专用菜单继续复用 `BaseView`/C++ action bridge,避免页面迁移造成功能回退。
|
||||||
|
- Home/Favorites 单引擎路径默认开启;设置环境变量 `QEMBY_USE_LEGACY_HOME_PAGES=1` 可回退旧双 `QuickPageHost`,用于一个发布周期内的兼容验证。
|
||||||
|
- 首批无服务器 offscreen 冒烟和 legacy fallback 冒烟均可稳定运行至测试超时,未出现 QML runtime error。期间发现并修复 `AdminTranscodingPage.DoubleField` 覆盖 `QQuickItem` final `bottom/top` 属性的问题。
|
||||||
|
|
||||||
|
2026-07-15 完成 Phase 2 第二批 Search:
|
||||||
|
|
||||||
|
- `Router.qml` 增加 Search 静态组件;Search 与 Home/Favorites 共享根 engine、图片 provider 和 action bridge。
|
||||||
|
- `HomeView::triggerSearch()` 改为 push 规范 search route;同一 Router widget 内切换也会记录历史,因此 Home → Search → Back 可以恢复 Home,Search → legacy Detail → Back 可以恢复原搜索。
|
||||||
|
- `SearchViewModel` 增加 active/generation 取消语义;inactive 时只记录待搜索 query,重新激活后按需恢复,避免隐藏页面回写陈旧结果。
|
||||||
|
- 导航契约测试新增 search key 和返回用例;默认 Router 与 legacy fallback 的无服务器 offscreen 冒烟均无 runtime 输出。
|
||||||
|
|
||||||
|
2026-07-15 完成 Phase 2 第三批 Category/Library/Person/Filtered:
|
||||||
|
|
||||||
|
- 根 Router 静态注册 CategoryPage 与 LibraryPage;Library、Person、Filtered 复用同一页面类型,但 route key 变化时重新创建 QML 页面。
|
||||||
|
- HomeView 的分类、媒体库、文件夹、人物和过滤入口统一 push 规范 route;legacy fallback 仍调用原 `create*View()`。
|
||||||
|
- Favorite Category 将 `favorite` 纳入规范 key,避免与普通 Category 使用相同身份。
|
||||||
|
- Category/Library ViewModel 的 inactive 路由更新只保存最新参数;generation 同时覆盖详情探测、首屏、渐进加载和 loadMore,防止离开后陈旧回写。
|
||||||
|
- 当前每类参数化页面仍共享一个会话 ViewModel;已加载 route 返回时复用数据,切换 route 时重建状态。每个历史 route 独立的 LRU 实例和滚动 uiState 尚未实现。
|
||||||
|
- 导航测试增加 Favorite Category 身份用例;Debug 构建、qmllint、默认 Router 与 legacy fallback 无服务器启动冒烟通过。
|
||||||
@@ -9,7 +9,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.6
|
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
|
||||||
```
|
```
|
||||||
|
|||||||
+102
-1
@@ -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)
|
||||||
@@ -10,6 +10,8 @@ set(APP_QRC_FILES resources/resources.qrc)
|
|||||||
|
|
||||||
file(GLOB_RECURSE APP_HEADERS CONFIGURE_DEPENDS "*.h")
|
file(GLOB_RECURSE APP_HEADERS CONFIGURE_DEPENDS "*.h")
|
||||||
file(GLOB_RECURSE APP_SOURCES CONFIGURE_DEPENDS "*.cpp")
|
file(GLOB_RECURSE APP_SOURCES CONFIGURE_DEPENDS "*.cpp")
|
||||||
|
list(FILTER APP_HEADERS EXCLUDE REGEX "/tests/")
|
||||||
|
list(FILTER APP_SOURCES EXCLUDE REGEX "/tests/")
|
||||||
|
|
||||||
qt_add_executable(qEmbyApp
|
qt_add_executable(qEmbyApp
|
||||||
MANUAL_FINALIZATION
|
MANUAL_FINALIZATION
|
||||||
@@ -19,6 +21,100 @@ qt_add_executable(qEmbyApp
|
|||||||
${APP_QRC_FILES}
|
${APP_QRC_FILES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# QML 模块:Widgets 混合迁移的 Quick 页面
|
||||||
|
# QML 页面通过 QuickPageHost (QQuickWidget) 嵌入现有页面栈;
|
||||||
|
# C++ 类型 (Theme 单例等) 通过 QML_ELEMENT 自动注册到 qEmby 模块。
|
||||||
|
# ==========================================
|
||||||
|
if(COMMAND qt_policy)
|
||||||
|
qt_policy(SET QTP0004 NEW)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
qt_add_qml_module(qEmbyApp
|
||||||
|
URI qEmby
|
||||||
|
VERSION 1.0
|
||||||
|
RESOURCE_PREFIX /qt/qml
|
||||||
|
QML_FILES
|
||||||
|
qml/AppShell.qml
|
||||||
|
qml/Router.qml
|
||||||
|
qml/TitleBar.qml
|
||||||
|
qml/SideBar.qml
|
||||||
|
qml/components/SearchHistoryPopup.qml
|
||||||
|
qml/ManagePage.qml
|
||||||
|
qml/ManageDashboardPage.qml
|
||||||
|
qml/LoginPage.qml
|
||||||
|
qml/SearchPage.qml
|
||||||
|
qml/CategoryPage.qml
|
||||||
|
qml/FavoritesPage.qml
|
||||||
|
qml/DashboardPage.qml
|
||||||
|
qml/LibraryPage.qml
|
||||||
|
qml/DetailPage.qml
|
||||||
|
qml/SeasonPage.qml
|
||||||
|
qml/PlayerPage.qml
|
||||||
|
qml/IndependentPlayerWindow.qml
|
||||||
|
qml/AdminUsersPage.qml
|
||||||
|
qml/AdminTasksPage.qml
|
||||||
|
qml/AdminLibrariesPage.qml
|
||||||
|
qml/AdminLogsPage.qml
|
||||||
|
qml/AdminCollectionsPage.qml
|
||||||
|
qml/AdminTranscodingPage.qml
|
||||||
|
qml/EmptyState.qml
|
||||||
|
qml/SettingsPage.qml
|
||||||
|
qml/SettingsAboutPage.qml
|
||||||
|
qml/SettingsGeneralPage.qml
|
||||||
|
qml/SettingsAppearancePage.qml
|
||||||
|
qml/SettingsLibraryPage.qml
|
||||||
|
qml/SettingsPlayerPage.qml
|
||||||
|
qml/components/SettingsCard.qml
|
||||||
|
qml/components/PageHeader.qml
|
||||||
|
qml/components/SegmentedTabs.qml
|
||||||
|
qml/components/CollapsibleSection.qml
|
||||||
|
qml/components/ThemedButton.qml
|
||||||
|
qml/components/ThemedSwitch.qml
|
||||||
|
qml/components/ThemedComboBox.qml
|
||||||
|
qml/components/ThemedTextField.qml
|
||||||
|
qml/components/ThemedSpinBox.qml
|
||||||
|
qml/components/ThemedSlider.qml
|
||||||
|
qml/components/ThemedScrollBar.qml
|
||||||
|
qml/components/ThemedDialog.qml
|
||||||
|
qml/components/ConfirmDialog.qml
|
||||||
|
qml/components/ToastHost.qml
|
||||||
|
qml/components/LoadingState.qml
|
||||||
|
qml/components/ErrorState.qml
|
||||||
|
qml/components/MediaCard.qml
|
||||||
|
qml/components/MediaGrid.qml
|
||||||
|
qml/components/MediaList.qml
|
||||||
|
qml/components/MediaSection.qml
|
||||||
|
qml/MpvSpikePage.qml
|
||||||
|
qml/MpvSpikeHarness.qml
|
||||||
|
SOURCES
|
||||||
|
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/settingsviewmodel.h quick/settingsviewmodel.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> 形式包含头文件
|
||||||
|
target_include_directories(qEmbyApp PRIVATE quick)
|
||||||
|
|
||||||
|
option(QEMBY_BUILD_NAVIGATION_TESTS
|
||||||
|
"Build the lightweight NavigationController contract test" ON)
|
||||||
|
if(QEMBY_BUILD_NAVIGATION_TESTS)
|
||||||
|
add_executable(qEmbyNavigationTest EXCLUDE_FROM_ALL
|
||||||
|
tests/navigationcontroller_test.cpp
|
||||||
|
quick/navigationcontroller.h quick/navigationcontroller.cpp
|
||||||
|
quick/navigationroute.h quick/navigationroute.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(qEmbyNavigationTest PRIVATE quick)
|
||||||
|
target_link_libraries(qEmbyNavigationTest PRIVATE Qt${QT_VERSION_MAJOR}::Core)
|
||||||
|
endif()
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 集成 libmpv 播放器核心
|
# 集成 libmpv 播放器核心
|
||||||
# Linux / macOS 优先使用系统安装的 libmpv
|
# Linux / macOS 优先使用系统安装的 libmpv
|
||||||
@@ -65,8 +161,13 @@ target_link_libraries(qEmbyApp PRIVATE
|
|||||||
Qt${QT_VERSION_MAJOR}::OpenGLWidgets
|
Qt${QT_VERSION_MAJOR}::OpenGLWidgets
|
||||||
Qt${QT_VERSION_MAJOR}::Svg
|
Qt${QT_VERSION_MAJOR}::Svg
|
||||||
Qt${QT_VERSION_MAJOR}::Concurrent
|
Qt${QT_VERSION_MAJOR}::Concurrent
|
||||||
|
Qt${QT_VERSION_MAJOR}::Quick
|
||||||
|
Qt${QT_VERSION_MAJOR}::Qml
|
||||||
|
Qt${QT_VERSION_MAJOR}::QuickWidgets
|
||||||
|
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
|
|
||||||
@@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
|
#include <QPropertyAnimation>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QStyle>
|
#include <QStyle>
|
||||||
#include <QStyleOptionButton>
|
#include <QStyleOptionButton>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "../tokens/AnimationTokens.h"
|
||||||
|
#include "../utils/motionprefs.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
class SectionToggleButton : public QPushButton {
|
class SectionToggleButton : public QPushButton {
|
||||||
public:
|
public:
|
||||||
@@ -94,6 +98,16 @@ CollapsibleSection::CollapsibleSection(const QString &title, QWidget *parent)
|
|||||||
m_contentLayout->setSpacing(8);
|
m_contentLayout->setSpacing(8);
|
||||||
mainLayout->addWidget(m_content);
|
mainLayout->addWidget(m_content);
|
||||||
|
|
||||||
|
m_heightAnim = new QPropertyAnimation(m_content, "maximumHeight", this);
|
||||||
|
m_heightAnim->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||||
|
connect(m_heightAnim, &QPropertyAnimation::finished, this, [this]() {
|
||||||
|
if (m_expanded) {
|
||||||
|
m_content->setMaximumHeight(QWIDGETSIZE_MAX);
|
||||||
|
} else {
|
||||||
|
m_content->setVisible(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
updateToggleText();
|
updateToggleText();
|
||||||
|
|
||||||
connect(m_toggle, &QPushButton::clicked, this,
|
connect(m_toggle, &QPushButton::clicked, this,
|
||||||
@@ -101,9 +115,33 @@ CollapsibleSection::CollapsibleSection(const QString &title, QWidget *parent)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CollapsibleSection::setExpanded(bool expanded) {
|
void CollapsibleSection::setExpanded(bool expanded) {
|
||||||
|
if (m_expanded == expanded) {
|
||||||
|
updateToggleText();
|
||||||
|
return;
|
||||||
|
}
|
||||||
m_expanded = expanded;
|
m_expanded = expanded;
|
||||||
|
|
||||||
|
m_heightAnim->stop();
|
||||||
|
if (MotionPrefs::reduceMotion()) {
|
||||||
|
m_content->setMaximumHeight(QWIDGETSIZE_MAX);
|
||||||
m_content->setVisible(expanded);
|
m_content->setVisible(expanded);
|
||||||
updateToggleText();
|
updateToggleText();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expanded) {
|
||||||
|
m_content->setMaximumHeight(0);
|
||||||
|
m_content->setVisible(true);
|
||||||
|
m_heightAnim->setEasingCurve(AnimationTokens::Curve::enter());
|
||||||
|
m_heightAnim->setStartValue(0);
|
||||||
|
m_heightAnim->setEndValue(m_content->sizeHint().height());
|
||||||
|
} else {
|
||||||
|
m_heightAnim->setEasingCurve(AnimationTokens::Curve::exit());
|
||||||
|
m_heightAnim->setStartValue(m_content->height());
|
||||||
|
m_heightAnim->setEndValue(0);
|
||||||
|
}
|
||||||
|
m_heightAnim->start();
|
||||||
|
updateToggleText();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CollapsibleSection::updateToggleText() {
|
void CollapsibleSection::updateToggleText() {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
|
class QPropertyAnimation;
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
class QVBoxLayout;
|
class QVBoxLayout;
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ private:
|
|||||||
QPushButton* m_toggle;
|
QPushButton* m_toggle;
|
||||||
QWidget* m_content;
|
QWidget* m_content;
|
||||||
QVBoxLayout* m_contentLayout;
|
QVBoxLayout* m_contentLayout;
|
||||||
|
QPropertyAnimation* m_heightAnim = nullptr;
|
||||||
bool m_expanded = false;
|
bool m_expanded = false;
|
||||||
QString m_title;
|
QString m_title;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,306 +0,0 @@
|
|||||||
#include "collectioncard.h"
|
|
||||||
#include "collectiongrid.h"
|
|
||||||
#include "../managers/thememanager.h"
|
|
||||||
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QMouseEvent>
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QPainterPath>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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"));
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
|
|
||||||
QColor cardBg = isDark ? QColor(51, 65, 85, 140) : QColor(255, 255, 255, 245);
|
|
||||||
QColor border = isDark ? QColor(71, 85, 105, 100) : QColor(226, 232, 240);
|
|
||||||
QColor nameFg = isDark ? QColor("#F1F5F9") : QColor("#0F172A");
|
|
||||||
QColor typeFg = isDark ? QColor("#94A3B8") : QColor("#64748B");
|
|
||||||
const QColor accent("#3B82F6");
|
|
||||||
|
|
||||||
if (m_selected) {
|
|
||||||
cardBg = isDark ? QColor(30, 41, 59, 220) : QColor(239, 246, 255, 245);
|
|
||||||
border = isDark ? QColor(96, 165, 250, 220) : QColor(59, 130, 246, 220);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_dragging) {
|
|
||||||
cardBg = isDark ? QColor(15, 23, 42, 235) : QColor(248, 250, 252, 252);
|
|
||||||
border = isDark ? QColor(59, 130, 246, 235) : QColor(37, 99, 235, 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 ? QColor(100, 116, 139, 160)
|
|
||||||
: QColor(148, 163, 184, 180)),
|
|
||||||
1.5));
|
|
||||||
p.setBrush(m_selected ? accent
|
|
||||||
: (isDark ? QColor(15, 23, 42, 180)
|
|
||||||
: QColor(255, 255, 255, 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 ? QColor(30, 41, 59) : QColor(241, 245, 249));
|
|
||||||
grad.setColorAt(1, isDark ? QColor(51, 65, 85) : QColor(226, 232, 240));
|
|
||||||
|
|
||||||
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(isDark ? QColor(148, 163, 184, 80) : QColor(100, 116, 139, 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::toggleSelected()
|
|
||||||
{
|
|
||||||
m_selected = !m_selected;
|
|
||||||
update();
|
|
||||||
emit selectionChanged(m_itemId, m_selected);
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
#ifndef COLLECTIONCARD_H
|
|
||||||
#define COLLECTIONCARD_H
|
|
||||||
|
|
||||||
#include <QFrame>
|
|
||||||
#include <QPoint>
|
|
||||||
#include <QPixmap>
|
|
||||||
#include <QPushButton>
|
|
||||||
|
|
||||||
class CollectionGrid;
|
|
||||||
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;
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,335 +0,0 @@
|
|||||||
#include "collectiongrid.h"
|
|
||||||
#include "collectioncard.h"
|
|
||||||
|
|
||||||
#include <QPropertyAnimation>
|
|
||||||
#include <QResizeEvent>
|
|
||||||
#include <QScrollArea>
|
|
||||||
#include <QScrollBar>
|
|
||||||
#include <QSet>
|
|
||||||
#include <QTimer>
|
|
||||||
|
|
||||||
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 && card->pos() != target) {
|
|
||||||
stopPosAnimations(card);
|
|
||||||
|
|
||||||
auto* anim = new QPropertyAnimation(card, "pos", card);
|
|
||||||
anim->setDuration(220);
|
|
||||||
anim->setEasingCurve(QEasingCurve::OutCubic);
|
|
||||||
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);
|
|
||||||
auto* anim = new QPropertyAnimation(card, "pos", card);
|
|
||||||
anim->setDuration(180);
|
|
||||||
anim->setEasingCurve(QEasingCurve::OutCubic);
|
|
||||||
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,560 +0,0 @@
|
|||||||
#include "dashboardsectionorderstrip.h"
|
|
||||||
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QEvent>
|
|
||||||
#include <QGraphicsOpacityEffect>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QMouseEvent>
|
|
||||||
#include <QPropertyAnimation>
|
|
||||||
#include <QResizeEvent>
|
|
||||||
#include <QStyle>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
constexpr int StripHeight = 36;
|
|
||||||
constexpr int ChipHeight = 28;
|
|
||||||
constexpr int ChipSpacing = 8;
|
|
||||||
constexpr int ChipHorizontalPadding = 12;
|
|
||||||
constexpr int DragPreviewYOffset = 5;
|
|
||||||
constexpr int ChipAnimationDuration = 220;
|
|
||||||
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 || widget->pos() == targetPos) {
|
|
||||||
widget->move(targetPos);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* animation = new QPropertyAnimation(widget, "pos", widget);
|
|
||||||
animation->setDuration(duration);
|
|
||||||
animation->setEasingCurve(QEasingCurve::OutCubic);
|
|
||||||
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,450 +0,0 @@
|
|||||||
#include "detailactionwidget.h"
|
|
||||||
#include "../managers/externalplayerdetector.h"
|
|
||||||
#include "../managers/thememanager.h"
|
|
||||||
#include "../utils/mediasourcepreferenceutils.h"
|
|
||||||
#include "../utils/playerpreferenceutils.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,
|
|
||||||
&DetailActionWidget::favoriteRequested);
|
|
||||||
connect(m_playedBtn, &QPushButton::clicked, this,
|
|
||||||
&DetailActionWidget::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,91 +0,0 @@
|
|||||||
#include "detailcontentwidget.h"
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QStyleOption>
|
|
||||||
#include <QLinearGradient>
|
|
||||||
|
|
||||||
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) {
|
|
||||||
m_backdrop = pix;
|
|
||||||
m_cachedWidth = -1;
|
|
||||||
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.drawPixmap(0, 0, m_scaledBackdrop);
|
|
||||||
|
|
||||||
|
|
||||||
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,39 +0,0 @@
|
|||||||
#ifndef DETAILCONTENTWIDGET_H
|
|
||||||
#define DETAILCONTENTWIDGET_H
|
|
||||||
|
|
||||||
#include <QWidget>
|
|
||||||
#include <QPixmap>
|
|
||||||
#include <QColor>
|
|
||||||
|
|
||||||
class DetailContentWidget : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
Q_PROPERTY(QColor baseColor READ baseColor WRITE setBaseColor)
|
|
||||||
Q_PROPERTY(QColor gradientColor READ gradientColor WRITE setGradientColor)
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void paintEvent(QPaintEvent* event) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
QPixmap m_backdrop;
|
|
||||||
QPixmap m_scaledBackdrop;
|
|
||||||
int m_cachedWidth;
|
|
||||||
|
|
||||||
|
|
||||||
QColor m_baseColor;
|
|
||||||
QColor m_gradientColor;
|
|
||||||
};
|
|
||||||
|
|
||||||
#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,515 +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 <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(QEasingCurve::OutCubic);
|
|
||||||
m_hScrollAnim->setDuration(450);
|
|
||||||
|
|
||||||
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());
|
|
||||||
|
|
||||||
auto* anim = new QPropertyAnimation(bar, "value", this);
|
|
||||||
anim->setDuration(400);
|
|
||||||
anim->setEasingCurve(QEasingCurve::OutCubic);
|
|
||||||
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()) {
|
|
||||||
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();
|
|
||||||
m_shimmer->show();
|
|
||||||
m_shimmer->startAnimation();
|
|
||||||
} 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))) {
|
|
||||||
m_btnLeft->hide();
|
|
||||||
m_btnRight->hide();
|
|
||||||
}
|
|
||||||
} 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();
|
|
||||||
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)) {
|
|
||||||
m_btnLeft->hide();
|
|
||||||
m_btnRight->hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int currentWidth = this->width();
|
|
||||||
QScrollBar* bar = m_listView->horizontalScrollBar();
|
|
||||||
|
|
||||||
bool isLeftHalf = localPos.x() < (currentWidth / 2);
|
|
||||||
|
|
||||||
m_btnLeft->setVisible(isLeftHalf && bar->value() > 0);
|
|
||||||
m_btnRight->setVisible(!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,255 +0,0 @@
|
|||||||
#include "horizontalwidgetgallery.h"
|
|
||||||
#include <QScrollArea>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QScrollBar>
|
|
||||||
#include <QPropertyAnimation>
|
|
||||||
#include <QEvent>
|
|
||||||
#include <QWheelEvent>
|
|
||||||
#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(QEasingCurve::OutCubic);
|
|
||||||
m_hScrollAnim->setDuration(450);
|
|
||||||
|
|
||||||
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());
|
|
||||||
|
|
||||||
auto* anim = new QPropertyAnimation(bar, "value", this);
|
|
||||||
anim->setDuration(400);
|
|
||||||
anim->setEasingCurve(QEasingCurve::OutCubic);
|
|
||||||
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))) {
|
|
||||||
m_btnLeft->hide();
|
|
||||||
m_btnRight->hide();
|
|
||||||
}
|
|
||||||
} 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();
|
|
||||||
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)) {
|
|
||||||
m_btnLeft->hide();
|
|
||||||
m_btnRight->hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int currentWidth = this->width();
|
|
||||||
QScrollBar* bar = m_scrollArea->horizontalScrollBar();
|
|
||||||
|
|
||||||
bool isLeftHalf = localPos.x() < (currentWidth / 2);
|
|
||||||
|
|
||||||
m_btnLeft->setVisible(isLeftHalf && bar->value() > 0);
|
|
||||||
m_btnRight->setVisible(!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,88 +0,0 @@
|
|||||||
#include "libraryactionmenu.h"
|
|
||||||
|
|
||||||
#include "../managers/thememanager.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);
|
|
||||||
|
|
||||||
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,8 +1,9 @@
|
|||||||
#include "loadingoverlay.h"
|
#include "loadingoverlay.h"
|
||||||
|
#include "../tokens/AnimationTokens.h"
|
||||||
|
#include "../utils/motionprefs.h"
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
#include <QPropertyAnimation>
|
#include <QPropertyAnimation>
|
||||||
#include <QEasingCurve>
|
|
||||||
#include <QConicalGradient>
|
#include <QConicalGradient>
|
||||||
#include <QLinearGradient>
|
#include <QLinearGradient>
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
@@ -14,17 +15,26 @@ LoadingOverlay::LoadingOverlay(QWidget* parent) : QWidget(parent) {
|
|||||||
m_rotateAnim = new QPropertyAnimation(this, "angle", this);
|
m_rotateAnim = new QPropertyAnimation(this, "angle", this);
|
||||||
m_rotateAnim->setStartValue(0.0);
|
m_rotateAnim->setStartValue(0.0);
|
||||||
m_rotateAnim->setEndValue(360.0);
|
m_rotateAnim->setEndValue(360.0);
|
||||||
m_rotateAnim->setDuration(1000);
|
m_rotateAnim->setDuration(AnimationTokens::DurationMs::kGentle);
|
||||||
m_rotateAnim->setLoopCount(-1);
|
m_rotateAnim->setLoopCount(-1);
|
||||||
|
|
||||||
m_fadeAnim = new QPropertyAnimation(this, "opacity", this);
|
m_fadeAnim = new QPropertyAnimation(this, "opacity", this);
|
||||||
m_fadeAnim->setDuration(300);
|
m_fadeAnim->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||||
m_fadeAnim->setEasingCurve(QEasingCurve::InOutQuad);
|
m_fadeAnim->setEasingCurve(AnimationTokens::Curve::fade());
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoadingOverlay::start() {
|
void LoadingOverlay::start() {
|
||||||
show();
|
show();
|
||||||
raise();
|
raise();
|
||||||
|
|
||||||
|
if (MotionPrefs::reduceMotion()) {
|
||||||
|
// Reduce-motion: static indicator, no spin, no fade.
|
||||||
|
m_fadeAnim->stop();
|
||||||
|
m_opacity = 1.0;
|
||||||
|
update();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
m_rotateAnim->start();
|
m_rotateAnim->start();
|
||||||
|
|
||||||
m_fadeAnim->disconnect();
|
m_fadeAnim->disconnect();
|
||||||
@@ -34,6 +44,11 @@ void LoadingOverlay::start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void LoadingOverlay::stop() {
|
void LoadingOverlay::stop() {
|
||||||
|
if (MotionPrefs::reduceMotion()) {
|
||||||
|
forceStop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
m_fadeAnim->disconnect();
|
m_fadeAnim->disconnect();
|
||||||
connect(m_fadeAnim, &QAbstractAnimation::finished, this, [this]() {
|
connect(m_fadeAnim, &QAbstractAnimation::finished, this, [this]() {
|
||||||
hide();
|
hide();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "../utils/mediaidentifyutils.h"
|
#include "../utils/mediaidentifyutils.h"
|
||||||
#include "../utils/mediaitemutils.h"
|
#include "../utils/mediaitemutils.h"
|
||||||
#include "../utils/playlistutils.h"
|
#include "../utils/playlistutils.h"
|
||||||
|
#include "../utils/popupfade.h"
|
||||||
#include <qembycore.h>
|
#include <qembycore.h>
|
||||||
#include <services/manager/servermanager.h>
|
#include <services/manager/servermanager.h>
|
||||||
#include <models/profile/serverprofile.h>
|
#include <models/profile/serverprofile.h>
|
||||||
@@ -33,6 +34,8 @@ MediaActionMenu::MediaActionMenu(const MediaItem& item, QEmbyCore* core,
|
|||||||
|
|
||||||
setAttribute(Qt::WA_TranslucentBackground);
|
setAttribute(Qt::WA_TranslucentBackground);
|
||||||
|
|
||||||
|
PopupFade::attach(this);
|
||||||
|
|
||||||
setupMenu();
|
setupMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,402 +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>
|
|
||||||
|
|
||||||
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(160);
|
|
||||||
|
|
||||||
|
|
||||||
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();
|
|
||||||
m_shimmer->show();
|
|
||||||
m_shimmer->startAnimation();
|
|
||||||
} 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()) {
|
|
||||||
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,4 +1,6 @@
|
|||||||
#include "moderncombobox.h"
|
#include "moderncombobox.h"
|
||||||
|
#include "../utils/fadehelper.h"
|
||||||
|
#include "../utils/popupfade.h"
|
||||||
#include <QAbstractItemView>
|
#include <QAbstractItemView>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QEvent>
|
#include <QEvent>
|
||||||
@@ -132,6 +134,8 @@ void ModernComboBox::showPopup() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PopupFade::run(popupContainer);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
auto *vScrollBar = popupView->verticalScrollBar();
|
auto *vScrollBar = popupView->verticalScrollBar();
|
||||||
@@ -267,6 +271,8 @@ void ModernComboBox::showEmbeddedPopup() {
|
|||||||
overlay->show();
|
overlay->show();
|
||||||
overlay->raise();
|
overlay->raise();
|
||||||
listFrame->show();
|
listFrame->show();
|
||||||
|
// Child overlay (not a top-level window) — FadeHelper, not PopupFade.
|
||||||
|
FadeHelper::fadeIn(listFrame, AnimationTokens::DurationMs::kQuick);
|
||||||
listWidget->setFocus();
|
listWidget->setFocus();
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,198 +0,0 @@
|
|||||||
#include "modernmenubutton.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);
|
|
||||||
|
|
||||||
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,4 +1,5 @@
|
|||||||
#include "modernslider.h"
|
#include "modernslider.h"
|
||||||
|
#include "../utils/hoveranimator.h"
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
|
|
||||||
@@ -7,6 +8,9 @@ ModernSlider::ModernSlider(Qt::Orientation orientation, QWidget *parent)
|
|||||||
setCursor(Qt::PointingHandCursor);
|
setCursor(Qt::PointingHandCursor);
|
||||||
setMouseTracking(true);
|
setMouseTracking(true);
|
||||||
setAttribute(Qt::WA_Hover, true);
|
setAttribute(Qt::WA_Hover, true);
|
||||||
|
|
||||||
|
m_hover = new HoverAnimator(this);
|
||||||
|
m_hover->setCallback([this](qreal) { update(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void ModernSlider::setBufferValue(int value) {
|
void ModernSlider::setBufferValue(int value) {
|
||||||
@@ -56,17 +60,19 @@ void ModernSlider::mouseReleaseEvent(QMouseEvent *ev) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ModernSlider::mouseMoveEvent(QMouseEvent *ev) {
|
void ModernSlider::mouseMoveEvent(QMouseEvent *ev) {
|
||||||
|
if (!m_isPressed) {
|
||||||
|
emit hoverMoved(ev->pos());
|
||||||
|
}
|
||||||
QSlider::mouseMoveEvent(ev);
|
QSlider::mouseMoveEvent(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool ModernSlider::event(QEvent *ev) {
|
bool ModernSlider::event(QEvent *ev) {
|
||||||
if (ev->type() == QEvent::Enter) {
|
if (ev->type() == QEvent::Enter) {
|
||||||
m_isHovered = true;
|
m_hover->setHovered(true);
|
||||||
update();
|
|
||||||
} else if (ev->type() == QEvent::Leave) {
|
} else if (ev->type() == QEvent::Leave) {
|
||||||
m_isHovered = false;
|
m_hover->setHovered(false);
|
||||||
update();
|
emit hoverLeft();
|
||||||
}
|
}
|
||||||
return QSlider::event(ev);
|
return QSlider::event(ev);
|
||||||
}
|
}
|
||||||
@@ -77,10 +83,12 @@ void ModernSlider::paintEvent(QPaintEvent *ev) {
|
|||||||
QPainter painter(this);
|
QPainter painter(this);
|
||||||
painter.setRenderHint(QPainter::Antialiasing);
|
painter.setRenderHint(QPainter::Antialiasing);
|
||||||
|
|
||||||
const int trackHeight =
|
// Pressed shows the active size instantly; hover grows/shrinks smoothly.
|
||||||
(m_isHovered || m_isPressed) ? m_hoverTrackHeight : m_normalTrackHeight;
|
const qreal hp = m_isPressed ? 1.0 : m_hover->progress();
|
||||||
const int handleRadius =
|
const qreal trackHeight =
|
||||||
(m_isHovered || m_isPressed) ? m_activeHandleRadius : m_normalHandleRadius;
|
m_normalTrackHeight + hp * (m_hoverTrackHeight - m_normalTrackHeight);
|
||||||
|
const qreal handleRadius =
|
||||||
|
m_normalHandleRadius + hp * (m_activeHandleRadius - m_normalHandleRadius);
|
||||||
const int edgePadding = qMax(m_normalHandleRadius, m_activeHandleRadius);
|
const int edgePadding = qMax(m_normalHandleRadius, m_activeHandleRadius);
|
||||||
|
|
||||||
QRectF trackRect;
|
QRectF trackRect;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
class HoverAnimator;
|
||||||
|
|
||||||
class ModernSlider : public QSlider {
|
class ModernSlider : public QSlider {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -62,6 +64,12 @@ public:
|
|||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
// Cursor position while hovering the (un-pressed) slider — drives hover
|
||||||
|
// affordances like the player's seek time bubble.
|
||||||
|
void hoverMoved(const QPoint &pos);
|
||||||
|
void hoverLeft();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void mousePressEvent(QMouseEvent *ev) override;
|
void mousePressEvent(QMouseEvent *ev) override;
|
||||||
void mouseReleaseEvent(QMouseEvent *ev) override;
|
void mouseReleaseEvent(QMouseEvent *ev) override;
|
||||||
@@ -71,8 +79,8 @@ protected:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
int m_bufferValue = 0;
|
int m_bufferValue = 0;
|
||||||
bool m_isHovered = false;
|
|
||||||
bool m_isPressed = false;
|
bool m_isPressed = false;
|
||||||
|
HoverAnimator *m_hover = nullptr;
|
||||||
|
|
||||||
|
|
||||||
QColor m_themeColor = QColor("#3B82F6");
|
QColor m_themeColor = QColor("#3B82F6");
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
#include "modernsortbutton.h"
|
|
||||||
#include <QAction>
|
|
||||||
|
|
||||||
ModernSortButton::ModernSortButton(QWidget *parent)
|
|
||||||
: QPushButton(parent), m_currentIndex(-1), m_descending(true)
|
|
||||||
{
|
|
||||||
m_menu = new QMenu(this);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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,4 +1,6 @@
|
|||||||
#include "modernswitch.h"
|
#include "modernswitch.h"
|
||||||
|
#include "../tokens/AnimationTokens.h"
|
||||||
|
#include "../utils/motionprefs.h"
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
|
|
||||||
ModernSwitch::ModernSwitch(QWidget* parent)
|
ModernSwitch::ModernSwitch(QWidget* parent)
|
||||||
@@ -14,8 +16,10 @@ ModernSwitch::ModernSwitch(QWidget* parent)
|
|||||||
|
|
||||||
|
|
||||||
m_animation = new QPropertyAnimation(this, "offset", this);
|
m_animation = new QPropertyAnimation(this, "offset", this);
|
||||||
m_animation->setDuration(150);
|
m_animation->setDuration(AnimationTokens::DurationMs::kMicro);
|
||||||
m_animation->setEasingCurve(QEasingCurve::InOutSine);
|
// A binary toggle wants a decisive settle, not the flat slow-in/slow-out of
|
||||||
|
// InOutSine; OutCubic reads as a proper switch.
|
||||||
|
m_animation->setEasingCurve(AnimationTokens::Curve::enter());
|
||||||
|
|
||||||
connect(this, &ModernSwitch::toggled, this, &ModernSwitch::updateAnimation);
|
connect(this, &ModernSwitch::toggled, this, &ModernSwitch::updateAnimation);
|
||||||
}
|
}
|
||||||
@@ -110,6 +114,10 @@ void ModernSwitch::leaveEvent(QEvent* event) {
|
|||||||
|
|
||||||
void ModernSwitch::updateAnimation(bool checked) {
|
void ModernSwitch::updateAnimation(bool checked) {
|
||||||
m_animation->stop();
|
m_animation->stop();
|
||||||
|
if (MotionPrefs::reduceMotion()) {
|
||||||
|
setOffset(checked ? 1.0 : 0.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
m_animation->setStartValue(m_offset);
|
m_animation->setStartValue(m_offset);
|
||||||
m_animation->setEndValue(checked ? 1.0 : 0.0);
|
m_animation->setEndValue(checked ? 1.0 : 0.0);
|
||||||
m_animation->start();
|
m_animation->start();
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QStyleOption>
|
#include <QStyleOption>
|
||||||
#include <QEasingCurve>
|
#include <QEasingCurve>
|
||||||
|
#include "../tokens/AnimationTokens.h"
|
||||||
|
#include "../utils/motionprefs.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -66,12 +68,12 @@ ModernToast::ModernToast(QWidget *parent)
|
|||||||
|
|
||||||
|
|
||||||
m_showOpacity = new QPropertyAnimation(this, "windowOpacity", this);
|
m_showOpacity = new QPropertyAnimation(this, "windowOpacity", this);
|
||||||
m_showOpacity->setDuration(200);
|
m_showOpacity->setDuration(AnimationTokens::DurationMs::kFast);
|
||||||
m_showOpacity->setEasingCurve(QEasingCurve::OutQuad);
|
m_showOpacity->setEasingCurve(AnimationTokens::Curve::fade());
|
||||||
|
|
||||||
m_showSlide = new QPropertyAnimation(this, "geometry", this);
|
m_showSlide = new QPropertyAnimation(this, "geometry", this);
|
||||||
m_showSlide->setDuration(350);
|
m_showSlide->setDuration(AnimationTokens::DurationMs::kSlow);
|
||||||
QEasingCurve slideCurve(QEasingCurve::OutBack);
|
QEasingCurve slideCurve(AnimationTokens::Curve::bounce());
|
||||||
slideCurve.setOvershoot(1.2);
|
slideCurve.setOvershoot(1.2);
|
||||||
m_showSlide->setEasingCurve(slideCurve);
|
m_showSlide->setEasingCurve(slideCurve);
|
||||||
|
|
||||||
@@ -83,16 +85,16 @@ ModernToast::ModernToast(QWidget *parent)
|
|||||||
|
|
||||||
|
|
||||||
m_crtSquashY = new QPropertyAnimation(this, "geometry", this);
|
m_crtSquashY = new QPropertyAnimation(this, "geometry", this);
|
||||||
m_crtSquashY->setDuration(160);
|
m_crtSquashY->setDuration(AnimationTokens::DurationMs::kFast);
|
||||||
m_crtSquashY->setEasingCurve(QEasingCurve::InCubic);
|
m_crtSquashY->setEasingCurve(AnimationTokens::Curve::exit());
|
||||||
|
|
||||||
m_crtSquashX = new QPropertyAnimation(this, "geometry", this);
|
m_crtSquashX = new QPropertyAnimation(this, "geometry", this);
|
||||||
m_crtSquashX->setDuration(120);
|
m_crtSquashX->setDuration(AnimationTokens::DurationMs::kMicro);
|
||||||
m_crtSquashX->setEasingCurve(QEasingCurve::InExpo);
|
m_crtSquashX->setEasingCurve(AnimationTokens::Curve::exit());
|
||||||
|
|
||||||
m_hideOpacity = new QPropertyAnimation(this, "windowOpacity", this);
|
m_hideOpacity = new QPropertyAnimation(this, "windowOpacity", this);
|
||||||
m_hideOpacity->setDuration(120);
|
m_hideOpacity->setDuration(AnimationTokens::DurationMs::kMicro);
|
||||||
m_hideOpacity->setEasingCurve(QEasingCurve::InCubic);
|
m_hideOpacity->setEasingCurve(AnimationTokens::Curve::exit());
|
||||||
|
|
||||||
m_crtPhase2Group = new QParallelAnimationGroup(this);
|
m_crtPhase2Group = new QParallelAnimationGroup(this);
|
||||||
m_crtPhase2Group->addAnimation(m_crtSquashX);
|
m_crtPhase2Group->addAnimation(m_crtSquashX);
|
||||||
@@ -116,6 +118,19 @@ ModernToast::ModernToast(QWidget *parent)
|
|||||||
m_stayTimer = new QTimer(this);
|
m_stayTimer = new QTimer(this);
|
||||||
m_stayTimer->setSingleShot(true);
|
m_stayTimer->setSingleShot(true);
|
||||||
connect(m_stayTimer, &QTimer::timeout, this, [this]() {
|
connect(m_stayTimer, &QTimer::timeout, this, [this]() {
|
||||||
|
if (MotionPrefs::reduceMotion()) {
|
||||||
|
// Reduce-motion: dismiss instantly, no CRT squash.
|
||||||
|
m_hideGroup->stop();
|
||||||
|
m_isCrtFadingOut = false;
|
||||||
|
m_comboCount = 0;
|
||||||
|
setWindowOpacity(0.0);
|
||||||
|
hide();
|
||||||
|
if (graphicsEffect()) {
|
||||||
|
graphicsEffect()->setEnabled(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
m_isCrtFadingOut = true;
|
m_isCrtFadingOut = true;
|
||||||
update();
|
update();
|
||||||
|
|
||||||
@@ -143,29 +158,29 @@ ModernToast::ModernToast(QWidget *parent)
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
QEasingCurve expandCurve(QEasingCurve::OutBack);
|
QEasingCurve expandCurve(AnimationTokens::Curve::bounce());
|
||||||
expandCurve.setOvershoot(2.5);
|
expandCurve.setOvershoot(2.5);
|
||||||
|
|
||||||
m_shellExpand = new QPropertyAnimation(this, "geometry", this);
|
m_shellExpand = new QPropertyAnimation(this, "geometry", this);
|
||||||
m_shellExpand->setDuration(160);
|
m_shellExpand->setDuration(AnimationTokens::DurationMs::kFast);
|
||||||
m_shellExpand->setEasingCurve(expandCurve);
|
m_shellExpand->setEasingCurve(expandCurve);
|
||||||
|
|
||||||
m_textExpand = new QPropertyAnimation(this, "textScale", this);
|
m_textExpand = new QPropertyAnimation(this, "textScale", this);
|
||||||
m_textExpand->setDuration(160);
|
m_textExpand->setDuration(AnimationTokens::DurationMs::kFast);
|
||||||
m_textExpand->setEasingCurve(expandCurve);
|
m_textExpand->setEasingCurve(expandCurve);
|
||||||
|
|
||||||
m_expandGroup = new QParallelAnimationGroup(this);
|
m_expandGroup = new QParallelAnimationGroup(this);
|
||||||
m_expandGroup->addAnimation(m_shellExpand);
|
m_expandGroup->addAnimation(m_shellExpand);
|
||||||
m_expandGroup->addAnimation(m_textExpand);
|
m_expandGroup->addAnimation(m_textExpand);
|
||||||
|
|
||||||
QEasingCurve shrinkCurve(QEasingCurve::OutCubic);
|
QEasingCurve shrinkCurve(AnimationTokens::Curve::enter());
|
||||||
|
|
||||||
m_shellShrink = new QPropertyAnimation(this, "geometry", this);
|
m_shellShrink = new QPropertyAnimation(this, "geometry", this);
|
||||||
m_shellShrink->setDuration(220);
|
m_shellShrink->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||||
m_shellShrink->setEasingCurve(shrinkCurve);
|
m_shellShrink->setEasingCurve(shrinkCurve);
|
||||||
|
|
||||||
m_textShrink = new QPropertyAnimation(this, "textScale", this);
|
m_textShrink = new QPropertyAnimation(this, "textScale", this);
|
||||||
m_textShrink->setDuration(220);
|
m_textShrink->setDuration(AnimationTokens::DurationMs::kNormal);
|
||||||
m_textShrink->setEasingCurve(shrinkCurve);
|
m_textShrink->setEasingCurve(shrinkCurve);
|
||||||
|
|
||||||
m_shrinkGroup = new QParallelAnimationGroup(this);
|
m_shrinkGroup = new QParallelAnimationGroup(this);
|
||||||
@@ -284,6 +299,23 @@ void ModernToast::showWithAnimation(const QString &msg, int durationMs) {
|
|||||||
|
|
||||||
show();
|
show();
|
||||||
|
|
||||||
|
if (MotionPrefs::reduceMotion()) {
|
||||||
|
// Reduce-motion: present instantly at final geometry, no slide/bounce/combo pop.
|
||||||
|
m_showGroup->stop();
|
||||||
|
m_popSequentialGroup->stop();
|
||||||
|
m_hideGroup->stop();
|
||||||
|
m_isCrtFadingOut = false;
|
||||||
|
m_comboCount = 0;
|
||||||
|
if (graphicsEffect()) {
|
||||||
|
graphicsEffect()->setEnabled(true);
|
||||||
|
}
|
||||||
|
setTextScale(1.0);
|
||||||
|
setGeometry(m_baseGeometry);
|
||||||
|
setWindowOpacity(1.0);
|
||||||
|
m_stayTimer->start(durationMs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
bool isFullyHidden = (windowOpacity() == 0.0 && m_showGroup->state() != QAbstractAnimation::Running);
|
bool isFullyHidden = (windowOpacity() == 0.0 && m_showGroup->state() != QAbstractAnimation::Running);
|
||||||
bool isFadingOut = (m_hideGroup->state() == QAbstractAnimation::Running);
|
bool isFadingOut = (m_hideGroup->state() == QAbstractAnimation::Running);
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ void MpvController::warmupOnce() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
QThreadPool::globalInstance()->start([]() {
|
QThreadPool::globalInstance()->start([]() {
|
||||||
@@ -131,7 +130,7 @@ bool MpvController::init() {
|
|||||||
|
|
||||||
mpv_set_option_string(m_mpv, "sub-hinting", "none");
|
mpv_set_option_string(m_mpv, "sub-hinting", "none");
|
||||||
mpv_set_option_string(m_mpv, "sub-ass-hinting", "none");
|
mpv_set_option_string(m_mpv, "sub-ass-hinting", "none");
|
||||||
mpv_request_log_messages(m_mpv, "info");
|
mpv_request_log_messages(m_mpv, "debug");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -146,7 +145,15 @@ bool MpvController::init() {
|
|||||||
<< "| interpolation=" << (vsyncMode == "display-resample");
|
<< "| interpolation=" << (vsyncMode == "display-resample");
|
||||||
|
|
||||||
|
|
||||||
|
mpv_set_option_string(m_mpv, "target-colorspace-hint", "auto");
|
||||||
|
mpv_set_option_string(m_mpv, "target-trc", "auto");
|
||||||
|
const QString toneMapping = ConfigStore::instance()->get<QString>(
|
||||||
|
ConfigKeys::PlayerToneMapping, "auto");
|
||||||
|
mpv_set_option_string(m_mpv, "tone-mapping", toneMapping.toUtf8().constData());
|
||||||
|
qDebug() << "[MpvController] Applied HDR tone mapping"
|
||||||
|
<< "| target-colorspace-hint=auto"
|
||||||
|
<< "| target-trc=auto"
|
||||||
|
<< "| tone-mapping=" << toneMapping;
|
||||||
|
|
||||||
QString hwdec = ConfigStore::instance()->get<QString>(ConfigKeys::PlayerHwDec, "auto-copy");
|
QString hwdec = ConfigStore::instance()->get<QString>(ConfigKeys::PlayerHwDec, "auto-copy");
|
||||||
mpv_set_option_string(m_mpv, "hwdec", hwdec.toUtf8().constData());
|
mpv_set_option_string(m_mpv, "hwdec", hwdec.toUtf8().constData());
|
||||||
@@ -217,12 +224,17 @@ bool MpvController::init() {
|
|||||||
observeProperty("eof-reached", MPV_FORMAT_FLAG);
|
observeProperty("eof-reached", MPV_FORMAT_FLAG);
|
||||||
|
|
||||||
observeProperty("cache-speed", MPV_FORMAT_INT64);
|
observeProperty("cache-speed", MPV_FORMAT_INT64);
|
||||||
|
observeProperty("demuxer-cache-time", MPV_FORMAT_DOUBLE);
|
||||||
observeProperty("volume", MPV_FORMAT_DOUBLE);
|
observeProperty("volume", MPV_FORMAT_DOUBLE);
|
||||||
observeProperty("mute", MPV_FORMAT_FLAG);
|
observeProperty("mute", MPV_FORMAT_FLAG);
|
||||||
observeProperty("speed", MPV_FORMAT_DOUBLE);
|
observeProperty("speed", MPV_FORMAT_DOUBLE);
|
||||||
observeProperty("track-list", MPV_FORMAT_NODE);
|
observeProperty("track-list", MPV_FORMAT_NODE);
|
||||||
observeProperty("sid", MPV_FORMAT_NODE);
|
observeProperty("sid", MPV_FORMAT_NODE);
|
||||||
observeProperty("secondary-sid", MPV_FORMAT_NODE);
|
observeProperty("secondary-sid", MPV_FORMAT_NODE);
|
||||||
|
observeProperty("video-params/dw", MPV_FORMAT_DOUBLE);
|
||||||
|
observeProperty("video-params/dh", MPV_FORMAT_DOUBLE);
|
||||||
|
observeProperty("width", MPV_FORMAT_INT64);
|
||||||
|
observeProperty("height", MPV_FORMAT_INT64);
|
||||||
|
|
||||||
|
|
||||||
observeProperty("paused-for-cache", MPV_FORMAT_FLAG);
|
observeProperty("paused-for-cache", MPV_FORMAT_FLAG);
|
||||||
@@ -289,7 +301,10 @@ void MpvController::eventHandler() {
|
|||||||
auto *msg = static_cast<mpv_event_log_message *>(event->data);
|
auto *msg = static_cast<mpv_event_log_message *>(event->data);
|
||||||
if (msg) {
|
if (msg) {
|
||||||
|
|
||||||
qDebug().noquote() << QString("[MPV %1] %2").arg(msg->prefix, QString::fromUtf8(msg->text).trimmed());
|
qWarning().noquote()
|
||||||
|
<< QString("[MPV %1] %2")
|
||||||
|
.arg(msg->prefix,
|
||||||
|
QString::fromUtf8(msg->text).trimmed());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QNetworkRequest>
|
#include <QNetworkRequest>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
|
#include <QStringList>
|
||||||
#include <QTcpServer>
|
#include <QTcpServer>
|
||||||
#include <QTcpSocket>
|
#include <QTcpSocket>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
@@ -55,7 +56,6 @@ QUrl MpvHttpStreamRelay::prepare(const QUrl &targetUrl, const QString &serverId,
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
stop();
|
|
||||||
if (!m_server->isListening() && !m_server->listen(QHostAddress::LocalHost, 0))
|
if (!m_server->isListening() && !m_server->listen(QHostAddress::LocalHost, 0))
|
||||||
{
|
{
|
||||||
qWarning() << "[MpvHttpStreamRelay] failed to listen"
|
qWarning() << "[MpvHttpStreamRelay] failed to listen"
|
||||||
@@ -66,9 +66,12 @@ QUrl MpvHttpStreamRelay::prepare(const QUrl &targetUrl, const QString &serverId,
|
|||||||
m_targetUrl = targetUrl;
|
m_targetUrl = targetUrl;
|
||||||
m_serverId = serverId;
|
m_serverId = serverId;
|
||||||
m_streamToken = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
m_streamToken = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
m_network->setProxy(proxy);
|
m_routes.insert(m_streamToken, RouteState{targetUrl, serverId, proxy});
|
||||||
m_bytesRelayedSinceLastTick = 0;
|
m_bytesRelayedSinceLastTick = 0;
|
||||||
|
if (!m_speedTimer->isActive())
|
||||||
|
{
|
||||||
m_speedTimer->start();
|
m_speedTimer->start();
|
||||||
|
}
|
||||||
|
|
||||||
QUrl localUrl;
|
QUrl localUrl;
|
||||||
localUrl.setScheme(QStringLiteral("http"));
|
localUrl.setScheme(QStringLiteral("http"));
|
||||||
@@ -102,6 +105,7 @@ void MpvHttpStreamRelay::stop()
|
|||||||
Q_EMIT upstreamSpeedChanged(0);
|
Q_EMIT upstreamSpeedChanged(0);
|
||||||
}
|
}
|
||||||
m_connections.clear();
|
m_connections.clear();
|
||||||
|
m_routes.clear();
|
||||||
m_targetUrl.clear();
|
m_targetUrl.clear();
|
||||||
m_serverId.clear();
|
m_serverId.clear();
|
||||||
m_streamToken.clear();
|
m_streamToken.clear();
|
||||||
@@ -176,7 +180,7 @@ void MpvHttpStreamRelay::onSocketDisconnected(QTcpSocket *socket)
|
|||||||
|
|
||||||
void MpvHttpStreamRelay::processRequest(QTcpSocket *socket, const QByteArray &requestData)
|
void MpvHttpStreamRelay::processRequest(QTcpSocket *socket, const QByteArray &requestData)
|
||||||
{
|
{
|
||||||
if (m_targetUrl.isEmpty() || m_streamToken.isEmpty())
|
if (m_routes.isEmpty())
|
||||||
{
|
{
|
||||||
writeError(socket, 503, "Relay target is not ready");
|
writeError(socket, 503, "Relay target is not ready");
|
||||||
return;
|
return;
|
||||||
@@ -198,14 +202,24 @@ void MpvHttpStreamRelay::processRequest(QTcpSocket *socket, const QByteArray &re
|
|||||||
|
|
||||||
const QByteArray method = requestParts.at(0).toUpper();
|
const QByteArray method = requestParts.at(0).toUpper();
|
||||||
const QByteArray path = requestParts.at(1);
|
const QByteArray path = requestParts.at(1);
|
||||||
const QByteArray expectedPrefix = QByteArray("/") + m_streamToken.toUtf8() + QByteArray("/");
|
QByteArray requestPath = path;
|
||||||
if (!(method == "GET" || method == "HEAD") || !path.startsWith(expectedPrefix))
|
const int queryStart = requestPath.indexOf('?');
|
||||||
|
if (queryStart >= 0)
|
||||||
|
{
|
||||||
|
requestPath.truncate(queryStart);
|
||||||
|
}
|
||||||
|
const QList<QByteArray> pathParts = requestPath.split('/');
|
||||||
|
const QByteArray tokenBytes = pathParts.size() >= 2 ? pathParts.at(1) : QByteArray();
|
||||||
|
const QString routeToken = QString::fromUtf8(tokenBytes);
|
||||||
|
const auto routeIt = m_routes.constFind(routeToken);
|
||||||
|
if (!(method == "GET" || method == "HEAD") || routeToken.isEmpty() || routeIt == m_routes.constEnd())
|
||||||
{
|
{
|
||||||
writeError(socket, 404, "Not found");
|
writeError(socket, 404, "Not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkRequest request(m_targetUrl);
|
const RouteState route = routeIt.value();
|
||||||
|
QNetworkRequest request(route.targetUrl);
|
||||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
|
|
||||||
for (int i = 1; i < lines.size(); ++i)
|
for (int i = 1; i < lines.size(); ++i)
|
||||||
@@ -239,12 +253,16 @@ void MpvHttpStreamRelay::processRequest(QTcpSocket *socket, const QByteArray &re
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
it->routeToken = routeToken;
|
||||||
|
it->targetUrl = route.targetUrl;
|
||||||
|
it->serverId = route.serverId;
|
||||||
it->headOnly = method == "HEAD";
|
it->headOnly = method == "HEAD";
|
||||||
|
m_network->setProxy(route.proxy);
|
||||||
it->reply = it->headOnly ? m_network->head(request) : m_network->get(request);
|
it->reply = it->headOnly ? m_network->head(request) : m_network->get(request);
|
||||||
it->reply->setReadBufferSize(kReplyReadBufferBytes);
|
it->reply->setReadBufferSize(kReplyReadBufferBytes);
|
||||||
|
|
||||||
qDebug() << "[MpvHttpStreamRelay] request"
|
qDebug() << "[MpvHttpStreamRelay] request"
|
||||||
<< "| method:" << method << "| target:" << LogRedactionUtils::url(m_targetUrl)
|
<< "| method:" << method << "| target:" << LogRedactionUtils::url(route.targetUrl)
|
||||||
<< "| range:" << request.rawHeader("Range");
|
<< "| range:" << request.rawHeader("Range");
|
||||||
|
|
||||||
QNetworkReply *reply = it->reply;
|
QNetworkReply *reply = it->reply;
|
||||||
@@ -293,7 +311,7 @@ void MpvHttpStreamRelay::processRequest(QTcpSocket *socket, const QByteArray &re
|
|||||||
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 0)
|
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 0)
|
||||||
{
|
{
|
||||||
qWarning() << "[MpvHttpStreamRelay] upstream failed"
|
qWarning() << "[MpvHttpStreamRelay] upstream failed"
|
||||||
<< "| target:" << LogRedactionUtils::url(m_targetUrl)
|
<< "| target:" << LogRedactionUtils::url(it->targetUrl)
|
||||||
<< "| error:" << reply->errorString();
|
<< "| error:" << reply->errorString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,6 +493,7 @@ void MpvHttpStreamRelay::closeConnection(QTcpSocket *socket)
|
|||||||
socket->disconnectFromHost();
|
socket->disconnectFromHost();
|
||||||
}
|
}
|
||||||
socket->deleteLater();
|
socket->deleteLater();
|
||||||
|
cleanupUnusedRoutes();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MpvHttpStreamRelay::recordRelayedBytes(qint64 bytes)
|
void MpvHttpStreamRelay::recordRelayedBytes(qint64 bytes)
|
||||||
@@ -485,6 +504,31 @@ void MpvHttpStreamRelay::recordRelayedBytes(qint64 bytes)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MpvHttpStreamRelay::cleanupUnusedRoutes()
|
||||||
|
{
|
||||||
|
QStringList activeTokens;
|
||||||
|
activeTokens.reserve(m_connections.size());
|
||||||
|
for (auto it = m_connections.cbegin(); it != m_connections.cend(); ++it)
|
||||||
|
{
|
||||||
|
if (!it->routeToken.isEmpty())
|
||||||
|
{
|
||||||
|
activeTokens.append(it->routeToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto it = m_routes.begin(); it != m_routes.end();)
|
||||||
|
{
|
||||||
|
if (it.key() != m_streamToken && !activeTokens.contains(it.key()))
|
||||||
|
{
|
||||||
|
it = m_routes.erase(it);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QByteArray MpvHttpStreamRelay::reasonPhrase(int statusCode)
|
QByteArray MpvHttpStreamRelay::reasonPhrase(int statusCode)
|
||||||
{
|
{
|
||||||
switch (statusCode)
|
switch (statusCode)
|
||||||
|
|||||||
@@ -29,11 +29,20 @@ private:
|
|||||||
struct ConnectionState {
|
struct ConnectionState {
|
||||||
QByteArray buffer;
|
QByteArray buffer;
|
||||||
QNetworkReply *reply = nullptr;
|
QNetworkReply *reply = nullptr;
|
||||||
|
QString routeToken;
|
||||||
|
QUrl targetUrl;
|
||||||
|
QString serverId;
|
||||||
bool headersSent = false;
|
bool headersSent = false;
|
||||||
bool headOnly = false;
|
bool headOnly = false;
|
||||||
bool upstreamFinished = false;
|
bool upstreamFinished = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct RouteState {
|
||||||
|
QUrl targetUrl;
|
||||||
|
QString serverId;
|
||||||
|
QNetworkProxy proxy;
|
||||||
|
};
|
||||||
|
|
||||||
void onNewConnection();
|
void onNewConnection();
|
||||||
void onSocketReadyRead(QTcpSocket *socket);
|
void onSocketReadyRead(QTcpSocket *socket);
|
||||||
void onSocketDisconnected(QTcpSocket *socket);
|
void onSocketDisconnected(QTcpSocket *socket);
|
||||||
@@ -43,6 +52,7 @@ private:
|
|||||||
void writeError(QTcpSocket *socket, int statusCode, const QByteArray &message);
|
void writeError(QTcpSocket *socket, int statusCode, const QByteArray &message);
|
||||||
void closeConnection(QTcpSocket *socket);
|
void closeConnection(QTcpSocket *socket);
|
||||||
void recordRelayedBytes(qint64 bytes);
|
void recordRelayedBytes(qint64 bytes);
|
||||||
|
void cleanupUnusedRoutes();
|
||||||
|
|
||||||
static QByteArray reasonPhrase(int statusCode);
|
static QByteArray reasonPhrase(int statusCode);
|
||||||
static bool isHopByHopHeader(QByteArray name);
|
static bool isHopByHopHeader(QByteArray name);
|
||||||
@@ -51,6 +61,7 @@ private:
|
|||||||
QNetworkAccessManager *m_network = nullptr;
|
QNetworkAccessManager *m_network = nullptr;
|
||||||
QTimer *m_speedTimer = nullptr;
|
QTimer *m_speedTimer = nullptr;
|
||||||
QHash<QTcpSocket *, ConnectionState> m_connections;
|
QHash<QTcpSocket *, ConnectionState> m_connections;
|
||||||
|
QHash<QString, RouteState> m_routes;
|
||||||
QUrl m_targetUrl;
|
QUrl m_targetUrl;
|
||||||
QString m_serverId;
|
QString m_serverId;
|
||||||
QString m_streamToken;
|
QString m_streamToken;
|
||||||
|
|||||||
@@ -1,296 +0,0 @@
|
|||||||
#include "mpvwidget.h"
|
|
||||||
#include "mpvhttpstreamrelay.h"
|
|
||||||
#include "../utils/logredactionutils.h"
|
|
||||||
#include <QOpenGLContext>
|
|
||||||
#include <QMetaObject>
|
|
||||||
#include <QCoreApplication>
|
|
||||||
#include <QSurfaceFormat>
|
|
||||||
#include <QUrl>
|
|
||||||
#include <QVariantMap>
|
|
||||||
#include "api/proxymanager.h"
|
|
||||||
|
|
||||||
MpvWidget::MpvWidget(QWidget *parent)
|
|
||||||
: QOpenGLWidget(parent), m_mpv_gl(nullptr) {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
QSurfaceFormat format = this->format();
|
|
||||||
|
|
||||||
format.setSwapInterval(1);
|
|
||||||
this->setFormat(format);
|
|
||||||
|
|
||||||
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() {
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MpvWidget::initializeGL() {
|
|
||||||
initializeOpenGLFunctions();
|
|
||||||
|
|
||||||
QOpenGLContext *ctx = QOpenGLContext::currentContext();
|
|
||||||
if (ctx && ctx->format().majorVersion() < 2) {
|
|
||||||
qCritical() << "Fatal Error: OpenGL version is too low.";
|
|
||||||
emit errorOccurred(tr("当前环境缺乏必需的 OpenGL 硬件加速支持,视频渲染已禁用。"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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,62 +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 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,813 +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>
|
|
||||||
|
|
||||||
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(QEasingCurve::OutCubic);
|
|
||||||
|
|
||||||
geometryAnim = new QPropertyAnimation(overlay, "geometry", this);
|
|
||||||
geometryAnim->setEasingCurve(QEasingCurve::OutCubic);
|
|
||||||
|
|
||||||
iconAnim = new QPropertyAnimation(iconLabel, "pos", this);
|
|
||||||
iconAnim->setDuration(220);
|
|
||||||
iconAnim->setEasingCurve(QEasingCurve::InOutSine);
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ? 120 : 180);
|
|
||||||
opacityAnim->setStartValue(startOpacity);
|
|
||||||
opacityAnim->setEndValue(targetOpacity);
|
|
||||||
|
|
||||||
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(120);
|
|
||||||
opacityAnim->setStartValue(opacityEffect->opacity());
|
|
||||||
opacityAnim->setEndValue(0.0);
|
|
||||||
|
|
||||||
geometryAnim->setDuration(120);
|
|
||||||
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 (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())
|
|
||||||
{
|
|
||||||
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,351 +0,0 @@
|
|||||||
#include "playerosdlayer.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(250);
|
|
||||||
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -134,6 +134,9 @@ QWidget *PlayerOverlayDialog::surfaceWidget() const
|
|||||||
return m_surface;
|
return m_surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: no FadeHelper here — m_surface carries a QGraphicsDropShadowEffect,
|
||||||
|
// and nesting a QGraphicsOpacityEffect on the parent breaks rendering
|
||||||
|
// ("QPainter not active" spam, invisible surface). Open/close stay instant.
|
||||||
void PlayerOverlayDialog::open()
|
void PlayerOverlayDialog::open()
|
||||||
{
|
{
|
||||||
syncOverlayGeometry();
|
syncOverlayGeometry();
|
||||||
|
|||||||
@@ -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,406 +0,0 @@
|
|||||||
#include "searchcompleterpopup.h"
|
|
||||||
|
|
||||||
#include "../managers/thememanager.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)
|
|
||||||
{
|
|
||||||
if (darkMode)
|
|
||||||
{
|
|
||||||
if (selected)
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
QColor(QStringLiteral("#163356")),
|
|
||||||
QColor(QStringLiteral("#102B49")),
|
|
||||||
QColor(QStringLiteral("#3C73A7")),
|
|
||||||
QColor(QStringLiteral("#F8FAFC")),
|
|
||||||
QColor(QStringLiteral("#BFDBFE")),
|
|
||||||
QColor(QStringLiteral("#1D446C")),
|
|
||||||
QColor(QStringLiteral("#BFDBFE"))};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hovered)
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
QColor(QStringLiteral("#162234")),
|
|
||||||
QColor(QStringLiteral("#101B2C")),
|
|
||||||
QColor(QStringLiteral("#28486B")),
|
|
||||||
QColor(QStringLiteral("#E6EEF8")),
|
|
||||||
QColor(QStringLiteral("#93C5FD")),
|
|
||||||
QColor(QStringLiteral("#17314E")),
|
|
||||||
QColor(QStringLiteral("#93C5FD"))};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
QColor(QStringLiteral("#111B2B")),
|
|
||||||
QColor(QStringLiteral("#0D1726")),
|
|
||||||
QColor(QStringLiteral("#1D2B3D")),
|
|
||||||
QColor(QStringLiteral("#CBD5E1")),
|
|
||||||
QColor(QStringLiteral("#93C5FD")),
|
|
||||||
QColor(QStringLiteral("#162233")),
|
|
||||||
QColor(QStringLiteral("#7F93AA"))};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selected)
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
QColor(QStringLiteral("#EAF2FF")),
|
|
||||||
QColor(QStringLiteral("#E2EEFF")),
|
|
||||||
QColor(QStringLiteral("#C6D9F6")),
|
|
||||||
QColor(QStringLiteral("#1E40AF")),
|
|
||||||
QColor(QStringLiteral("#1D4ED8")),
|
|
||||||
QColor(QStringLiteral("#DDE9FF")),
|
|
||||||
QColor(QStringLiteral("#1D4ED8"))};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hovered)
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
QColor(QStringLiteral("#F6FAFF")),
|
|
||||||
QColor(QStringLiteral("#EEF5FF")),
|
|
||||||
QColor(QStringLiteral("#D8E5F7")),
|
|
||||||
QColor(QStringLiteral("#284675")),
|
|
||||||
QColor(QStringLiteral("#2563EB")),
|
|
||||||
QColor(QStringLiteral("#EAF2FF")),
|
|
||||||
QColor(QStringLiteral("#2563EB"))};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
QColor(QStringLiteral("#F8FBFE")),
|
|
||||||
QColor(QStringLiteral("#F3F7FA")),
|
|
||||||
QColor(QStringLiteral("#E4EBF3")),
|
|
||||||
QColor(QStringLiteral("#334155")),
|
|
||||||
QColor(QStringLiteral("#2563EB")),
|
|
||||||
QColor(QStringLiteral("#EFF4FA")),
|
|
||||||
QColor(QStringLiteral("#7A8CA3"))};
|
|
||||||
}
|
|
||||||
|
|
||||||
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,626 +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>
|
|
||||||
|
|
||||||
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 = 150;
|
|
||||||
constexpr int kChipHoverDecorFadeOutDurationMs = 120;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChipPalette basePalette(const QString &heatLevel, bool darkMode)
|
|
||||||
{
|
|
||||||
if (darkMode)
|
|
||||||
{
|
|
||||||
if (heatLevel == QLatin1String("top"))
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
QColor(QStringLiteral("#23476D")),
|
|
||||||
QColor(QStringLiteral("#183B5F")),
|
|
||||||
QColor(QStringLiteral("#4F80B5")),
|
|
||||||
QColor(QStringLiteral("#F8FBFF")),
|
|
||||||
QColor(QStringLiteral("#315F93")),
|
|
||||||
QColor(QStringLiteral("#6499D1")),
|
|
||||||
QColor(QStringLiteral("#FFFFFF")),
|
|
||||||
QColor(QStringLiteral("#284F7B")),
|
|
||||||
QColor(QStringLiteral("#EAF3FF"))};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (heatLevel == QLatin1String("high"))
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
QColor(QStringLiteral("#1B3A59")),
|
|
||||||
QColor(QStringLiteral("#15314D")),
|
|
||||||
QColor(QStringLiteral("#3F71A4")),
|
|
||||||
QColor(QStringLiteral("#EAF3FF")),
|
|
||||||
QColor(QStringLiteral("#28527E")),
|
|
||||||
QColor(QStringLiteral("#5082B9")),
|
|
||||||
QColor(QStringLiteral("#F5FAFF")),
|
|
||||||
QColor(QStringLiteral("#24486F")),
|
|
||||||
QColor(QStringLiteral("#EAF3FF"))};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (heatLevel == QLatin1String("mid"))
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
QColor(QStringLiteral("#17304A")),
|
|
||||||
QColor(QStringLiteral("#122943")),
|
|
||||||
QColor(QStringLiteral("#355C84")),
|
|
||||||
QColor(QStringLiteral("#D6E7FF")),
|
|
||||||
QColor(QStringLiteral("#23486F")),
|
|
||||||
QColor(QStringLiteral("#4472A5")),
|
|
||||||
QColor(QStringLiteral("#E7F2FF")),
|
|
||||||
QColor(QStringLiteral("#203F61")),
|
|
||||||
QColor(QStringLiteral("#DDEBFF"))};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
QColor(QStringLiteral("#1B2535")),
|
|
||||||
QColor(QStringLiteral("#141E2D")),
|
|
||||||
QColor(QStringLiteral("#2F3D54")),
|
|
||||||
QColor(QStringLiteral("#D6E0EB")),
|
|
||||||
QColor(QStringLiteral("#253247")),
|
|
||||||
QColor(QStringLiteral("#43546C")),
|
|
||||||
QColor(QStringLiteral("#C8D4E2")),
|
|
||||||
QColor(QStringLiteral("#222F43")),
|
|
||||||
QColor(QStringLiteral("#CBD5E1"))};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (heatLevel == QLatin1String("top"))
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
QColor(QStringLiteral("#E2F0FF")),
|
|
||||||
QColor(QStringLiteral("#CFE2FF")),
|
|
||||||
QColor(QStringLiteral("#95B9F0")),
|
|
||||||
QColor(QStringLiteral("#173D8C")),
|
|
||||||
QColor(QStringLiteral("#CFE1FF")),
|
|
||||||
QColor(QStringLiteral("#95B9F0")),
|
|
||||||
QColor(QStringLiteral("#1D4ED8")),
|
|
||||||
QColor(QStringLiteral("#E9F2FF")),
|
|
||||||
QColor(QStringLiteral("#5A79B8"))};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (heatLevel == QLatin1String("high"))
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
QColor(QStringLiteral("#EDF5FF")),
|
|
||||||
QColor(QStringLiteral("#DDEBFF")),
|
|
||||||
QColor(QStringLiteral("#B9D2F8")),
|
|
||||||
QColor(QStringLiteral("#214AA5")),
|
|
||||||
QColor(QStringLiteral("#DDEBFF")),
|
|
||||||
QColor(QStringLiteral("#ABC8F6")),
|
|
||||||
QColor(QStringLiteral("#2563EB")),
|
|
||||||
QColor(QStringLiteral("#EEF4FC")),
|
|
||||||
QColor(QStringLiteral("#63758F"))};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (heatLevel == QLatin1String("mid"))
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
QColor(QStringLiteral("#F5F9FF")),
|
|
||||||
QColor(QStringLiteral("#E9F1FF")),
|
|
||||||
QColor(QStringLiteral("#CBDCF4")),
|
|
||||||
QColor(QStringLiteral("#2558BB")),
|
|
||||||
QColor(QStringLiteral("#EAF2FF")),
|
|
||||||
QColor(QStringLiteral("#C5D8F7")),
|
|
||||||
QColor(QStringLiteral("#2D69D1")),
|
|
||||||
QColor(QStringLiteral("#F2F6FB")),
|
|
||||||
QColor(QStringLiteral("#71839B"))};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
QColor(QStringLiteral("#FBFCFE")),
|
|
||||||
QColor(QStringLiteral("#F3F7FA")),
|
|
||||||
QColor(QStringLiteral("#DEE6EF")),
|
|
||||||
QColor(QStringLiteral("#334155")),
|
|
||||||
QColor(QStringLiteral("#FFFFFF")),
|
|
||||||
QColor(QStringLiteral("#D8E1EC")),
|
|
||||||
QColor(QStringLiteral("#64748B")),
|
|
||||||
QColor(QStringLiteral("#F3F6F9")),
|
|
||||||
QColor(QStringLiteral("#8090A4"))};
|
|
||||||
}
|
|
||||||
|
|
||||||
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(QEasingCurve::OutCubic);
|
|
||||||
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 ? QEasingCurve::OutCubic
|
|
||||||
: QEasingCurve::InCubic);
|
|
||||||
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,340 +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>
|
|
||||||
|
|
||||||
ServerWheelView::ServerWheelView(QWidget* parent) : QWidget(parent) {
|
|
||||||
setAttribute(Qt::WA_StyledBackground, true);
|
|
||||||
setStyleSheet("ServerWheelView { background: transparent; }");
|
|
||||||
|
|
||||||
m_scrollAnim = new QVariantAnimation(this);
|
|
||||||
|
|
||||||
m_scrollAnim->setEasingCurve(QEasingCurve::OutExpo);
|
|
||||||
m_scrollAnim->setDuration(450);
|
|
||||||
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();
|
|
||||||
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 <config/configstore.h>
|
|
||||||
#include <config/config_keys.h>
|
|
||||||
#include <QEasingCurve>
|
|
||||||
#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(250);
|
|
||||||
m_animation->setEasingCurve(QEasingCurve::OutCubic);
|
|
||||||
|
|
||||||
|
|
||||||
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,153 +0,0 @@
|
|||||||
#include "shimmerwidget.h"
|
|
||||||
#include "../managers/thememanager.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(1800);
|
|
||||||
m_animation->setLoopCount(-1);
|
|
||||||
m_animation->setEasingCurve(QEasingCurve::Linear);
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
m_shimmerPhase = 0.0;
|
|
||||||
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,4 +1,5 @@
|
|||||||
#include "slidingstackedwidget.h"
|
#include "slidingstackedwidget.h"
|
||||||
|
#include "../tokens/AnimationTokens.h"
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
@@ -17,8 +18,8 @@ QPoint interpolatePoint(const QPoint &start, const QPoint &end, qreal progress)
|
|||||||
|
|
||||||
SlidingStackedWidget::SlidingStackedWidget(QWidget *parent)
|
SlidingStackedWidget::SlidingStackedWidget(QWidget *parent)
|
||||||
: QStackedWidget(parent),
|
: QStackedWidget(parent),
|
||||||
m_speed(350),
|
m_speed(AnimationTokens::DurationMs::kSlow),
|
||||||
m_animationType(QEasingCurve::OutCubic),
|
m_animationType(AnimationTokens::Curve::enter().type()),
|
||||||
m_isAnimating(false),
|
m_isAnimating(false),
|
||||||
m_nextIndex(0)
|
m_nextIndex(0)
|
||||||
{
|
{
|
||||||
@@ -45,6 +46,35 @@ void SlidingStackedWidget::slideInWgt(QWidget *widget, SlideDirection direction)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SlidingStackedWidget::jumpToWgt(QWidget *widget)
|
||||||
|
{
|
||||||
|
int idx = indexOf(widget);
|
||||||
|
if (idx != -1) {
|
||||||
|
jumpToIdx(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SlidingStackedWidget::jumpToIdx(int idx)
|
||||||
|
{
|
||||||
|
if (idx < 0 || idx >= count())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (currentIndex() == idx && !m_isAnimating)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (m_isAnimating) {
|
||||||
|
m_slideTimer.stop();
|
||||||
|
animationDoneSlot();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_nextIndex = idx;
|
||||||
|
setCurrentIndex(idx);
|
||||||
|
QWidget *w = widget(idx);
|
||||||
|
if (w) w->raise();
|
||||||
|
flushPendingWidgetDisposals();
|
||||||
|
Q_EMIT animationFinished();
|
||||||
|
}
|
||||||
|
|
||||||
void SlidingStackedWidget::slideInIdx(int idx, SlideDirection direction)
|
void SlidingStackedWidget::slideInIdx(int idx, SlideDirection direction)
|
||||||
{
|
{
|
||||||
if (idx < 0 || idx >= count())
|
if (idx < 0 || idx >= count())
|
||||||
@@ -64,12 +94,7 @@ void SlidingStackedWidget::slideInIdx(int idx, SlideDirection direction)
|
|||||||
|
|
||||||
bool reduceAnimations = ConfigStore::instance()->get<bool>(ConfigKeys::UiAnimations, false);
|
bool reduceAnimations = ConfigStore::instance()->get<bool>(ConfigKeys::UiAnimations, false);
|
||||||
if (reduceAnimations) {
|
if (reduceAnimations) {
|
||||||
m_nextIndex = idx;
|
jumpToIdx(idx);
|
||||||
setCurrentIndex(idx);
|
|
||||||
QWidget *w = widget(idx);
|
|
||||||
if (w) w->raise();
|
|
||||||
flushPendingWidgetDisposals();
|
|
||||||
Q_EMIT animationFinished();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ public:
|
|||||||
|
|
||||||
void slideInIdx(int idx, SlideDirection direction = Automatic);
|
void slideInIdx(int idx, SlideDirection direction = Automatic);
|
||||||
void slideInWgt(QWidget *widget, SlideDirection direction = Automatic);
|
void slideInWgt(QWidget *widget, SlideDirection direction = Automatic);
|
||||||
|
// Instant switch (no slide), safely finishing any in-flight animation.
|
||||||
|
// For targets with their own entrance animation (e.g. the player view).
|
||||||
|
void jumpToIdx(int idx);
|
||||||
|
void jumpToWgt(QWidget *widget);
|
||||||
void disposeWidgetWhenSafe(QWidget *widget);
|
void disposeWidgetWhenSafe(QWidget *widget);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user