- 音视频
- 直播
- 移动开发
【免费下载链接】pure_live
纯粹直播:哔哩哔哩/虎牙/斗鱼/快手/抖音/网易cc/YY直播/Twitch直播/SOOP直播/M38自定义源应有尽有。
导读
本文基于 pure_live 仓库中的docs/TOOLBOX_ROUTE_AND_SELECTOR_LAYOUT_AUDIT_2026_09_12.md审计记录,完整还原工具箱(Link Parser)模块在"路由所有权"与"选择器布局"两个维度上的一次系统性回归排查与修订。读者将看到:剪贴板异步检测结果为何会在页面被覆盖后"回填错位"、清晰度/线路选择器在 320×480、3.0 倍文字下为何点击不命中,以及 pure_live 如何通过"页面上下文 + ModalRoute 身份 + ActionScope 动作作用域"三重约束修复这些问题。读完本文,你可以掌握一套可复用的 Flutter/GetX 路由安全异步回填与对话框可达性设计模式。
一、审计背景:范围、基线与方法
1.1 审计范围
本轮审计聚焦 pure_live 工具箱模块的三个高风险点:
- 首次剪贴板检测:工具箱页面打开时自动读取系统剪贴板,将检测到的直播链接回填到输入框;
- 页面被后续路由覆盖时的异步结果:剪贴板读取是异步操作,若用户在读取完成前导航到其他页面,迟到的结果可能污染已被覆盖的页面;
- 清晰度/线路选择器:获取直链流程中让用户选择清晰度与线路的对话框;
- 可达性:在 320×480 小屏、3.0 倍文字缩放下,中英文两种语言的界面操作是否仍可命中。
1.2 基线与方法
审计基线为提交f325121cb12d3093fdf1f7407ea15189b61412f2。方法上只做源码静态分析、确定性 Widget/控制器回归,未连接手机、未调用真实平台、未构建安装包或发布版本——这意味着结论全部建立在代码证据与确定性回归之上,而非设备实测。
二、问题复现:23/26 PASS 背后的三项有效失败
旧实现的两文件回归结果为23/26 PASS,其余三项失败正是本次审计要解决的核心问题:
- 路由覆盖后的迟到剪贴板回填:工具箱打开后用户立即进入其他页面,剪贴板异步结果返回时仍会回填已被覆盖页面的两个输入框("直播间跳转"与"获取直链"),并尝试弹出提示 Snackbar。
- 英文窄屏下清晰度首项不可命中:320×480、3.0 倍英文缩放下,清晰度列表首项中心落出可视区域,直接点击不命中,进行中的动作无法结束。
- 取消按钮随列表滚动出屏:同一窗口下取消按钮随
SimpleDialog.children排在内容末尾,滚动后仍可能位于根视图之外,选择器无法可靠关闭。
2.1 根因分析
审计记录给出了两条清晰的技术根因:
- 剪贴板检测缺路由所有权:旧实现由控制器在
onReady中无上下文启动剪贴板检测,只检查"控制器存活 + 输入修订号",不知道发起页面是否仍是当前路由。异步返回时页面早已被覆盖,但校验条件全部通过。 - 选择器缺高度边界:旧选择器把标题、全部选项与取消按钮放进同一个无明确高度边界的
SimpleDialog.children列表中。极端文字缩放时整条对话框内容可超出窗口高度,首项与取消按钮都落出可视/可点击区域。
三、修订方案:路由身份校验 + 安全区约束对话框
针对上述根因,审计修订落在四个层面:
3.1 剪贴板检测改由页面上下文启动
首次剪贴板检测不再由控制器在onReady自行启动,而是由工具箱页面在首帧后传入页面BuildContext,同时保留"单次读取 + 输入修订号"两道保护。源码中可看到这一改动的落地位置:
- toolbox_page.dart:
WidgetsBinding.instance.addPostFrameCallback在首帧回调中调用controller.autoCheckClipboard(context: context); - 控制器构造中仍保留
_clipboardChecked单次读取标志,以及_roomRevision/_urlRevision输入修订计数(toolbox_controller.dart)。
3.2 异步返回后的三重核对
剪贴板异步返回后,控制器同时核对三项条件(toolbox_controller.dart):
if (_disposed || isClosed || (context != null && (!context.mounted || sourceRoute == null || !sourceRoute.isCurrent))) { return; }即:控制器未销毁、页面BuildContext仍挂载、原始ModalRoute身份仍为当前路由(isCurrent)。被新页面覆盖或已退出的页面一律不回填、不显示提示。sourceRoute在检测启动前通过ModalRoute.of(context)捕获(第 253 行),保证校验的是发起时的原始路由对象而非页面内重新查询的结果。
3.3 输入修订号防"清空后回填"
除了路由所有权,修订还保留了修订号机制(toolbox_controller.dart):
final fillRoom = roomEmpty && _roomRevision == roomRevision; final fillUrl = urlEmpty && _urlRevision == urlRevision;审计代码注释明确指出:"输入再清空也是一次编辑"——若用户在剪贴板读取等待期间手动输入又清空了输入框,仅检查"当前文本为空"会被误判为"需要填充",从而把用户刻意清空的字段重新填满。修订号方案把"等待期间发生过编辑"视为放弃自动填充的充分条件。
3.4 选择器改为安全区约束的路由内对话框
清晰度/线路选择器从无高度边界的SimpleDialog重构为受安全区、键盘和窗口高度约束的路由内对话框(toolbox_controller.dart):
- 使用
DialogRoute(本项目 GetX 封装的PopupRoute,见 dialog_route.dart)以独立路由形式弹出; - 最大高度按
MediaQuery.size.height - padding.vertical - viewInsets.vertical - 32计算,下限钳制在 160,兼顾小屏与键盘弹出场景:final maxHeight = (mediaQuery.size.height - mediaQuery.padding.vertical - mediaQuery.viewInsets.vertical - 32) .clamp(160.0, double.infinity) .toDouble(); - 标题与全部选项共用可滚动内容区(
Flexible+SingleChildScrollView),取消动作固定于底部可点击区(Align+TextButton),不再随列表滚动; - 对话框最大宽度 560、
insetPadding横纵 16,窄屏下内容始终留在窗口内。
同时选项、取消与对话框继续受既有约束:动作作用域(ActionScope)、单次选择(selected标志)与路由所有权。取消按钮只移除自身对话框路由,不影响上层路由(Navigator.of(dialogContext).pop())。
四、源码级实现剖析
4.1 ActionScope:一次用户动作的生命周期容器
本次修订的全部异步动作都挂在ActionScope上(action_scope.dart)。它的核心契约是:
ownerAlive:回调返回动作所有者是否仍存活——工具箱场景下即为"控制器未销毁 + 页面仍挂载 + 源路由为当前路由(或自属对话框为当前路由)"(toolbox_controller.dart);timeout:默认 12 秒,超时以TimeoutException结束;cancelToken:取消令牌,cancel()后所有wait立即以ActionCancelled抛出;wait/waitCancellable:统一的"竞争式等待"原语,将超时、取消、完成三者用Future.any竞争(action_scope.dart)。
_runAction的finally块还保证了状态收敛(toolbox_controller.dart):scope.cancel()后只有"当前作用域仍是自己"时才清空action.value,避免旧动作清理新动作状态。
4.2 选择器与直链流水线的协作
选择器由getPlayUrl经LiveDirectLinkFlow.run驱动(direct_link_flow.dart)。整条流水线的时序为:
- 校验平台支持性 →
Sites.isSupported; getRoomDetail拉取房间详情;discoverPlayQualities发现清晰度列表(支持LiveQualityDiscovery的站点走waitCancellable网络取消路径);- 用户选择清晰度(无计时器,
timed: false,只有网络/平台操作才计时); resolvePlayUrls解析播放地址;- 若
inputRecipe != null(应用内会话专属源)则拒绝导出直链,提示使用应用内播放或录制; - 用户选择线路,最终复制到剪贴板并提示成功。
工具型清晰度/线路选择器位于工具箱控制器_choose<T>泛型方法,同一方法同时服务清晰度(LivePlayQuality)与线路(String)两种选择(toolbox_controller.dart)。它还在scope.cancelToken.whenCancel上挂接路由清理:动作被取消时若对话框路由仍活跃,则直接navigator.removeRoute(route)强制收场。
4.3 同一模式的复用:已知房间动作对话框
值得注意:同样的"路由所有权 + 安全区约束 + 动作作用域"模式也在播放器侧的已知房间动作对话框中落地(known_room_link_dialog.dart):
ownerAlive同时校验mounted、isCurrentRoom()、sourceRoute.isActive与对话框路由isCurrent(第 160-166 行);- 对话框同样以
availableHeight约束高度、SingleChildScrollView承载标题与选项、底部SafeArea+ 固定TextButton承载取消(第 308-363 行); - 该文件是播放器菜单与控制栏共享的"一次路由拥有的动作",与工具箱控制器形成可对照的双实现证据。
这印证了审计中"选项、取消和对话框受既有动作作用域、单次选择与路由所有权约束"并非单点补丁,而是项目级的一致模式。
4.4 剪贴板内容的链接判定
自动回填前,autoCheckClipboard用containsSupportedLink做本地检测(toolbox_controller.dart),与手动解析共用同一套 URI 提取逻辑LiveUrlTool.containsSupportedLink(live_url_tool.dart)。该方法覆盖 bilibili(含 b23.tv 短链)、虎牙、斗鱼、抖音(含 v.douyin.com、webcast.amemv.com reflow)、快手、网易 CC、Twitch、SOOP、YY、微博、小红书、Niconico、Kilakila、17LIVE、Showroom、CHZZK、LiveMe、TikTok、YouTube、Bigo、PandaLive、FC2、Steam Broadcast、京东、酷狗、百度、六间房、LOOK 等平台,并复用WebSearchRoomParser的同步房间标识校验,确保平台主页/分类/搜索页 URL 不会误触发回填。
五、窄屏可达性:320×480 与 3.0 倍文字
审计将窄屏中英文覆盖提升到3.0 倍文字,并明确了两个可验证标准:
- 首项可滚动到命中区域:选项列表在可滚动内容区中,用户总能通过滚动让首项进入可视区完成点击;
- 取消按钮无需滚动即可命中:取消固定在底部独立区域,不受列表滚动影响,任何情况下都可直接点击关闭。
这与"受安全区、键盘和窗口高度约束"的对话框结构直接对应——viewInsets.vertical项处理了键盘弹出侵占高度的情况,padding.vertical处理了刘海屏/状态栏安全区,clamp(160.0, …)保证极端缩放下仍有最低可用高度。
六、验证与回归结果
审计记录的验证链路分为四层:
| 层级 | 范围 | 结果 |
|---|---|---|
| 有效红测 | 旧实现两文件 | 23/26 PASS,失败项即路由覆盖、英文首项、英文取消动作 |
| 修订后定向回归 | 两文件 | 26/26 PASS |
| 联合回归 | 工具箱、链接解析、清晰度发现、已知房间动作共八文件 | 168/168 PASS |
| Focused CI | 最终 | 168/168 PASS;flutter analyze全库 No issues found(353.3 秒);仓库审计 errors=0 |
质量记录归档于local-artifacts/build-records/20260911T180847406Z-quality-focused.json,仓库审计记录归档于local-artifacts/repository-audits/20260911T175821820Z-focused.json(本轮工作目录中该路径对应记录未随仓库保留,归档名称与审计日期一致,可在对应 release 构建产物中追溯)。
6.1 验收边界
- A1-05(工具箱剪贴板和选择事务):已具备页面路由所有权、迟到结果隔离与联合回归证据,状态保持RUN;
- A2-01(工具箱主页面、清晰度和线路选择器可达性):320×480、3.0 倍中英文下操作保持可达,状态保持RUN;
- 宏观验收仍为20 PASS / 33 RUN / 9 NR,共 42 组未闭环;审计明确声明:本轮没有用源码证据替代最终候选双端操作,即在 Android 与 Windows 双端真机操作闭环之前,相关项不得提前判定为 PASS。
七、可复用的设计要点
从本次审计可以提炼出四条可迁移到其他页面的工程原则:
- 异步回填必须携带"发起上下文":任何在首帧/后台发起的读剪贴板、读缓存等异步任务,都应把发起页面的
BuildContext与ModalRoute快照一起传递,返回时校验context.mounted && sourceRoute.isCurrent,而不是仅依赖控制器存活。 - 编辑修订号是"用户反悔"的唯一可靠信号:等待期间用户输入、清空、再输入都应递增修订号;只比对当前文本会漏掉"清空"这一用户意图。
- 选择器对话框要显式声明高度边界:
MediaQuery.size.height - padding - viewInsets - 预留值并clamp下限;标题与选项进滚动区、操作按钮固定在底部,是保证极端文字缩放下仍可点击的最小结构。 - 动作作用域统一由
ActionScope管理:超时、取消令牌、所有者存活检查三者集中在一处,cancelToken.whenCancel上挂接路由清理,避免取消动作后留下孤儿对话框。
八、相关源码与文档索引
- 审计文档:docs/TOOLBOX_ROUTE_AND_SELECTOR_LAYOUT_AUDIT_2026_09_12.md
- 工具箱页面:lib/features/toolbox/toolbox_page.dart
- 工具箱控制器(剪贴板检测、动作作用域、选择器):lib/features/toolbox/toolbox_controller.dart
- 动作作用域原语:lib/core/utils/action_scope.dart
- 直链获取流水线:lib/domains/live/data/direct_link_flow.dart
- 已知房间动作对话框(同模式复用):lib/domains/live/presentation/playback/dialogs/known_room_link_dialog.dart
- GetX 对话框路由封装:lib/get/get_navigation/src/dialog/dialog_route.dart
- 链接解析与支持判定:lib/domains/live/data/link/live_url_tool.dart
- 工具箱路由注册与绑定:lib/app/router/app_pages.dart、lib/app/router/page_bindings.dart、lib/core/navigation/route_path.dart
- 播放器侧清晰度/线路选择器(对照实现):lib/domains/live/presentation/playback/widgets/resolution_selector/resolution_selector.dart、lib/domains/live/presentation/playback/widgets/resolution_selector/line_selector.dart
- 中英文文案定义:assets/translations/zh.json、assets/translations/en.json
- 音视频
- 直播
- 移动开发
【免费下载链接】pure_live
纯粹直播:哔哩哔哩/虎牙/斗鱼/快手/抖音/网易cc/YY直播/Twitch直播/SOOP直播/M38自定义源应有尽有。
相关推荐
pure_live 主题设置弹窗布局审计实战:窄屏溢出、间距校验与选择器重构
pure_live 主题设置弹窗布局审计实战:窄屏溢出、间距校验与选择器重构 导读 本文围绕 pure_live 项目中一次针对「主题设置二级长页 + 三个选择
音视频直播移动开发pure_live IPTV EPG 来源选择对话框修复实录:加载状态、迟到结果隔离与窄屏大字体适配
pure_live IPTV EPG 来源选择对话框修复实录:加载状态、迟到结果隔离与窄屏大字体适配 导读 本文围绕 pure_live 开源直播应用中 IPT
音视频直播移动开发pure_live 共享确认弹窗布局与路由重构审计:窄屏大字号溢出的根治方案
pure_live 共享确认弹窗布局与路由重构审计:窄屏大字号溢出的根治方案 导读 本文基于 pure_live 仓库中的《共享确认与消息弹窗布局及路由审计》文
音视频直播移动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考