fix(player): 修复竖滑上一集不自动播放
This commit is contained in:
1 parent
df36890b48
commit
516593499c
10 files changed
+264
-80
No files matched your search
@@ -969,7 +969,7 @@ class PlayerLogic extends ChangeNotifier {
|
||||
!isMounted()) {
|
||||
return;
|
||||
}
|
||||
if (!ready && ticketError != null) return;
|
||||
if (!ready) return;
|
||||
await _selectVisible(index);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -124,6 +125,8 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
double? _speedBeforeBoost;
|
||||
bool? _chromeBeforeBoost;
|
||||
DateTime? _lastDoubleTapAt;
|
||||
DateTime? _swipeStartedAt;
|
||||
bool _swipePrewarmCaptured = false;
|
||||
|
||||
/// 收藏切换后的本地增量:基准是详情快照,切换成功按服务端状态 ±1。
|
||||
final Map<String, int> _favoriteDelta = {};
|
||||
@@ -308,6 +311,34 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
unawaited(logic.setPlaybackSpeed(speed));
|
||||
}
|
||||
|
||||
bool _handlePageScroll(ScrollNotification notification) {
|
||||
if (notification is ScrollStartNotification &&
|
||||
notification.dragDetails != null) {
|
||||
_swipeStartedAt = DateTime.now();
|
||||
_swipePrewarmCaptured = false;
|
||||
} else if (notification is ScrollUpdateNotification &&
|
||||
!_swipePrewarmCaptured &&
|
||||
notification.dragDetails != null) {
|
||||
final delta = notification.scrollDelta ?? 0;
|
||||
if (delta != 0) {
|
||||
_swipePrewarmCaptured = true;
|
||||
logic.session.prewarmNeighbors(forward: delta > 0);
|
||||
}
|
||||
} else if (notification is ScrollEndNotification) {
|
||||
_swipePrewarmCaptured = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _logSwipeReveal() {
|
||||
if (!kDebugMode) return;
|
||||
final startedAt = _swipeStartedAt;
|
||||
if (startedAt == null) return;
|
||||
_swipeStartedAt = null;
|
||||
final elapsed = DateTime.now().difference(startedAt).inMilliseconds;
|
||||
debugPrint('[player] swipe reveal ${elapsed}ms');
|
||||
}
|
||||
|
||||
Future<void> _refreshHomeFeed() async {
|
||||
final refresh = widget.onRefreshFeed;
|
||||
if (refresh == null) return;
|
||||
@@ -420,25 +451,28 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
final content = AnimatedBuilder(
|
||||
animation: logic.session,
|
||||
builder: (context, _) {
|
||||
final pages = PageView.builder(
|
||||
controller: logic.pages,
|
||||
scrollDirection: Axis.vertical,
|
||||
allowImplicitScrolling: true,
|
||||
physics: widget.onRefreshFeed == null
|
||||
? null
|
||||
: const FeedRefreshPhysics(),
|
||||
itemCount: indices.length,
|
||||
onPageChanged: (position) {
|
||||
final index = indices[position];
|
||||
widget.onDramaChanged?.call(logic.queue.entryAt(index).drama);
|
||||
unawaited(logic.selectEpisode(index));
|
||||
if (position >= indices.length - 3) {
|
||||
final loadMore = widget.onLoadMore;
|
||||
if (loadMore != null) unawaited(loadMore());
|
||||
}
|
||||
},
|
||||
itemBuilder: (context, position) =>
|
||||
_buildEpisode(context, indices[position], interactions),
|
||||
final pages = NotificationListener<ScrollNotification>(
|
||||
onNotification: _handlePageScroll,
|
||||
child: PageView.builder(
|
||||
controller: logic.pages,
|
||||
scrollDirection: Axis.vertical,
|
||||
allowImplicitScrolling: true,
|
||||
physics: widget.onRefreshFeed == null
|
||||
? null
|
||||
: const FeedRefreshPhysics(),
|
||||
itemCount: indices.length,
|
||||
onPageChanged: (position) {
|
||||
final index = indices[position];
|
||||
widget.onDramaChanged?.call(logic.queue.entryAt(index).drama);
|
||||
unawaited(logic.selectEpisode(index));
|
||||
if (position >= indices.length - 3) {
|
||||
final loadMore = widget.onLoadMore;
|
||||
if (loadMore != null) unawaited(loadMore());
|
||||
}
|
||||
},
|
||||
itemBuilder: (context, position) =>
|
||||
_buildEpisode(context, indices[position], interactions),
|
||||
),
|
||||
);
|
||||
if (widget.onRefreshFeed == null) return pages;
|
||||
return FeedPullRefresh(
|
||||
@@ -550,6 +584,7 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
final handoffPort = handoff is VideoPlayerPort ? handoff : null;
|
||||
// 测试播放器没有纹理。有播放器但不是视频控制器时视为已就绪,避免加载圈一直转。
|
||||
final videoReady = videoPort?.controller.value.isInitialized ?? false;
|
||||
if (selected && videoReady) _logSwipeReveal();
|
||||
final ready = videoReady || port != null;
|
||||
final theater = logic.pip || logic.pipPreparing;
|
||||
final locked = logic.session.isLocked || episode.locked;
|
||||
@@ -577,7 +612,10 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
handoff: handoffPort?.controller,
|
||||
videoUrl: videoPort?.controller.dataSource ?? episode.videoUrl,
|
||||
theater: theater,
|
||||
openingFrame: playerOpeningFrame(episode.thumbnailsUrl),
|
||||
openingFrame: playerOpeningFrame(
|
||||
episode.thumbnailsUrl,
|
||||
posterUrl: drama.posterUrl,
|
||||
),
|
||||
),
|
||||
if (selected &&
|
||||
!theater &&
|
||||
|
||||
@@ -8,25 +8,33 @@ import 'package:flutter/material.dart';
|
||||
import '../../../core/config/app_config.dart';
|
||||
import '../../../core/data/services/sprite_thumbnail_asset_loader.dart';
|
||||
import '../../../core/data/services/sprite_thumbnail_axis.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/widgets/remote_image.dart';
|
||||
import 'scrub_preview_card.dart';
|
||||
|
||||
/// 无雪碧图地址时不垫帧,交还给纯黑底。
|
||||
Widget? playerOpeningFrame(String? thumbnailsUrl) {
|
||||
/// 封面立即可见,雪碧图首帧解码完成后再覆盖封面。
|
||||
Widget? playerOpeningFrame(String? thumbnailsUrl, {String? posterUrl}) {
|
||||
final url = thumbnailsUrl?.trim() ?? '';
|
||||
if (url.isEmpty) return null;
|
||||
return PlayerOpeningFrame(thumbnailsUrl: url);
|
||||
final poster = posterUrl?.trim() ?? '';
|
||||
if (url.isEmpty && poster.isEmpty) return null;
|
||||
return PlayerOpeningFrame(
|
||||
thumbnailsUrl: url.isEmpty ? null : url,
|
||||
posterUrl: poster.isEmpty ? null : poster,
|
||||
);
|
||||
}
|
||||
|
||||
/// 解码出视频画面前,用本集雪碧图第一帧垫底。
|
||||
/// 视频解码前依次使用封面和雪碧图首帧,最终由父层黑色兜底。
|
||||
class PlayerOpeningFrame extends StatefulWidget {
|
||||
const PlayerOpeningFrame({
|
||||
super.key,
|
||||
required this.thumbnailsUrl,
|
||||
this.thumbnailsUrl,
|
||||
this.posterUrl,
|
||||
this.loader,
|
||||
this.cache,
|
||||
});
|
||||
|
||||
final String thumbnailsUrl;
|
||||
final String? thumbnailsUrl;
|
||||
final String? posterUrl;
|
||||
final SpriteThumbnailAssetLoader? loader;
|
||||
final OpeningFrameCache? cache;
|
||||
|
||||
@@ -55,8 +63,10 @@ class _PlayerOpeningFrameState extends State<PlayerOpeningFrame> {
|
||||
|
||||
Future<void> _load() async {
|
||||
final serial = ++_serial;
|
||||
final thumbnailsUrl = widget.thumbnailsUrl;
|
||||
if (thumbnailsUrl == null) return;
|
||||
final frame = await (widget.cache ?? OpeningFrameCache.shared).load(
|
||||
widget.thumbnailsUrl,
|
||||
thumbnailsUrl,
|
||||
loader: widget.loader,
|
||||
);
|
||||
if (!mounted || serial != _serial) return;
|
||||
@@ -66,11 +76,25 @@ class _PlayerOpeningFrameState extends State<PlayerOpeningFrame> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final frame = _frame;
|
||||
if (frame == null) return const SizedBox.expand();
|
||||
return SpriteCropPreview(
|
||||
key: const Key('player-opening-sprite'),
|
||||
image: frame.image,
|
||||
sourceRect: frame.sourceRect,
|
||||
final posterUrl = widget.posterUrl;
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
const ColoredBox(color: AppColors.mediaBlack),
|
||||
if (posterUrl != null)
|
||||
RemoteImage(
|
||||
posterUrl,
|
||||
key: const Key('player-opening-poster'),
|
||||
fit: BoxFit.cover,
|
||||
fallbackColor: AppColors.mediaBlack,
|
||||
),
|
||||
if (frame != null)
|
||||
SpriteCropPreview(
|
||||
key: const Key('player-opening-sprite'),
|
||||
image: frame.image,
|
||||
sourceRect: frame.sourceRect,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user