Skip to content

feat: Add video preview on cover hover for desktop app#2334

Open
stephen-zeng wants to merge 5 commits into
bggRGjQaUbCoE:mainfrom
stephen-zeng:main
Open

feat: Add video preview on cover hover for desktop app#2334
stephen-zeng wants to merge 5 commits into
bggRGjQaUbCoE:mainfrom
stephen-zeng:main

Conversation

@stephen-zeng

Copy link
Copy Markdown
Contributor

变更说明

本 PR 为桌面端通用视频卡片新增“封面悬停预览”能力:鼠标悬停在视频封面超过配置时长后,自动以静音、最低画质播放视频预览,鼠标移出后恢复封面。模仿B站网页端和YouTube网页端首页的效果。
目前接入复用通用卡片的页面有推荐、搜索、热门、排行、相关视频。

主要改动

  • 新增统一封面预览控件 VideoCoverPreview,接入 VideoCardV 和 VideoCardH。
  • 新增应用级 VideoCoverPreviewController,复用单个 media_kit Player / VideoController。
  • 鼠标移出或卡片销毁时只停止/解绑当前预览,不销毁底层播放器,便于后续卡片快速复用。
  • 仅桌面端启用 hover 逻辑,移动端不注册悬停预览。
  • 首批仅支持普通 UGC 视频,排除直播、PUGV、PGC redirect 等接口差异较大的内容。
  • 预览视频层只覆盖封面区域,不接管点击、长按、右键、弹出菜单、badge、进度条等原卡片交互。

取流与账号

  • 缺少 cid 时通过 SearchHttp.ab2cWithDimension() 解析。
  • 通过 VideoHttp.videoUrl() 获取播放地址,优先选择 DASH video-only 最低可用画质,兜底 durl。
  • 预览请求使用请求级 AnonymousAccount() 覆盖账号。
  • 不修改全局 Accounts.heartbeat / Accounts.video。
  • 不调用 VideoHttp.heartBeat() 或 VideoHttp.historyReport(),避免预览产生历史、心跳或播放记录。

音频与系统媒体隔离

  • 预览播放器独立于详情页 PlPlayerController。
  • 不调用 PlPlayerController.play()。
  • 不激活系统 audio session。
  • 不注册锁屏、通知栏、后台音频或系统媒体控件。
  • 播放器显式静音,并只挂载视频轨,不加载 DASH audio。

全局设置

在桌面端“播放器设置”中新增“封面悬停预览”设置项:

  • 支持开关启用/关闭封面预览。
  • 默认开启。
  • 默认触发时长为 0.5s。
  • 支持设置 0.1s 到 3.0s,步进 0.1s。
  • 关闭开关时会停止当前预览并释放全局预览播放器。

预览显示效果

  • 预览画面使用黑色背景居中显示,不拉伸。
  • 竖屏视频按 9:16 或真实宽高比居中,左右留黑边。
  • 横屏视频按 16:9 或真实宽高比居中,上下留黑边。
  • 进入和退出预览均使用约 200ms 交叉叠化动画,避免视频层突兀出现或消失。
Screen.Recording.2026-06-06.at.5.33.57.PM.webm

Copilot AI review requested due to automatic review settings June 6, 2026 09:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a desktop-only “hover to preview video cover” feature with user-configurable delay, and threads an optional Account through relevant HTTP requests to support account-aware fetching.

Changes:

  • Added new settings keys/prefs for enabling cover hover preview and configuring preview trigger delay.
  • Introduced a VideoCoverPreview widget + global controller backed by media_kit for hover playback.
  • Updated VideoHttp.videoUrl and SearchHttp.ab2cWithDimension to accept an optional Account and pass it via request options.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
lib/utils/storage_pref.dart Adds new preference getters for preview enablement and delay.
lib/utils/storage_key.dart Adds new persistent setting keys for the preview feature.
lib/pages/setting/models/play_settings.dart Adds desktop setting row + delay configuration dialog.
lib/http/video.dart Adds optional Account param and forwards it via request options.
lib/http/search.dart Adds optional Account param and forwards it via request options.
lib/common/widgets/video_card/video_cover_preview_controller.dart Adds singleton controller managing a shared media_kit player instance.
lib/common/widgets/video_card/video_cover_preview.dart Adds hover-driven preview widget that resolves and plays a low-quality stream.
lib/common/widgets/video_card/video_card_v.dart Replaces static cover image with hover-preview-enabled cover widget (vertical cards).
lib/common/widgets/video_card/video_card_h.dart Replaces static cover image with hover-preview-enabled cover widget (horizontal cards).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +334 to +340
builder: (context, setDialogState) => Column(
spacing: 20,
mainAxisSize: MainAxisSize.min,
children: [
Slider(
padding: .zero,
value: delay,
Comment on lines +23 to +27
Future<void> play(Object owner, String url) async {
final sessionId = ++_sessionId;
await _ensurePlayer();
final player = _player;
if (player == null || sessionId != _sessionId) return;
Comment on lines +125 to +134
Future<void> _startPreview(int requestId) async {
if (!_isCurrentRequest(requestId) || _loading) return;
setState(() => _loading = true);

final preview = await _resolvePreview();
if (!_isCurrentRequest(requestId)) return;

if (preview == null) {
setState(() => _loading = false);
return;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants