// Author: fruit.com.cn import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; import '../../../core/data/models/drama.dart'; import '../../../core/data/services/playback_thumbnail_service.dart'; import '../../../core/data/services/sprite_thumbnail_axis.dart'; import '../../../core/theme/app_colors.dart'; import '../../../l10n/app_localizations.dart'; import 'home_feed_recommend_panel.dart'; import 'loading_progress_track.dart'; import 'seek_progress_bar.dart'; part 'home_feed_overlay_parts.dart'; const double _progressHeight = 2; const double _progressHitHeight = 28; const double _pageHorizontal = 14; const double _railRight = 8; const double _railGap = 12; const double _railBottomOffset = 60; const List _boostTextShadows = [ Shadow(color: AppColors.shadow, blurRadius: 5, offset: Offset(0, 1)), ]; /// 首页沉浸式 feed 浮层:手势层、右侧互动栏、底部信息与整集进度条。 class HomeFeedOverlay extends StatefulWidget { const HomeFeedOverlay({ super.key, required this.l10n, required this.drama, required this.episode, required this.controller, required this.locked, required this.liked, required this.saved, required this.visible, required this.boosting, required this.onLike, required this.onSave, required this.onComments, required this.onShare, required this.onDetail, required this.onOpenFullPlayer, this.onEpisodes, required this.onTogglePlay, required this.onBoostStart, required this.onBoostEnd, required this.onMore, this.likeCount, this.favoriteCount, this.commentCount, this.authorNote = '', this.onScrubStart, this.onScrubEnd, this.onScrubVisibilityChanged, this.onSeek, this.cancelScrubRequest = 0, this.thumbnailService, this.spriteAxisService, }); final AppLocalizations l10n; final Drama drama; final Episode episode; final ValueListenable? controller; final bool locked; final bool liked; final int? likeCount; final int? favoriteCount; final int? commentCount; final bool saved; final String authorNote; final bool visible; final bool boosting; final VoidCallback onLike; final VoidCallback onSave; final VoidCallback onComments; final VoidCallback onShare; final VoidCallback onDetail; final VoidCallback onOpenFullPlayer; final VoidCallback? onEpisodes; final VoidCallback onTogglePlay; final VoidCallback onBoostStart; final VoidCallback onBoostEnd; final VoidCallback onMore; final VoidCallback? onScrubStart; final VoidCallback? onScrubEnd; final ValueChanged? onScrubVisibilityChanged; final ValueChanged? onSeek; final int cancelScrubRequest; final PlaybackThumbnailService? thumbnailService; final SpriteThumbnailAxisService? spriteAxisService; @override State createState() => _HomeFeedOverlayState(); } class _HomeFeedOverlayState extends State { bool _scrubbing = false; bool _clearAfterScrub = false; @override void didUpdateWidget(covariant HomeFeedOverlay oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.cancelScrubRequest != widget.cancelScrubRequest) { _scrubbing = false; _clearAfterScrub = false; } } @override Widget build(BuildContext context) { return SafeArea( bottom: false, child: Stack( children: [ Positioned.fill( child: LayoutBuilder( builder: (context, constraints) => Listener( onPointerUp: (_) => widget.onBoostEnd(), onPointerCancel: (_) => widget.onBoostEnd(), child: GestureDetector( key: const Key('home-feed-gesture'), behavior: HitTestBehavior.opaque, onTap: _handleSurfaceTap, onLongPressStart: (details) => _handleLongPressStart(details, constraints.maxWidth), onLongPressEnd: (_) => widget.onBoostEnd(), onLongPressCancel: widget.onBoostEnd, ), ), ), ), if (widget.boosting) Positioned(left: 0, right: 0, bottom: 20, child: _boostHint()) else if (widget.visible && !_scrubbing && !_clearAfterScrub) Positioned( left: _pageHorizontal, right: _railRight, bottom: 12, child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded(child: _bottomInfo()), const SizedBox(width: _railGap), Padding( padding: const EdgeInsets.only(bottom: _railBottomOffset), child: _rail(), ), ], ), ), Positioned( left: 0, right: 0, bottom: 0, child: _FeedProgressBar( controller: widget.controller, locked: widget.locked, height: _progressHeight, hitHeight: _progressHitHeight, cancelScrubRequest: widget.cancelScrubRequest, onScrubStart: _startScrub, onScrubEnd: _endScrub, onSeek: widget.onSeek, ), ), ], ), ); } void _startScrub() { if (!_scrubbing || _clearAfterScrub) { setState(() { _scrubbing = true; _clearAfterScrub = false; }); } widget.onScrubVisibilityChanged?.call(true); widget.onScrubStart?.call(); } void _endScrub() { if (_scrubbing || !_clearAfterScrub) { setState(() { _scrubbing = false; _clearAfterScrub = true; }); } widget.onScrubEnd?.call(); } void _handleSurfaceTap() { if (_clearAfterScrub) { setState(() => _clearAfterScrub = false); widget.onScrubVisibilityChanged?.call(false); } widget.onTogglePlay(); } void _handleLongPressStart(LongPressStartDetails details, double width) { final third = width / 3; final dx = details.localPosition.dx; if (dx < third || dx > width - third) { widget.onBoostStart(); return; } widget.onMore(); } Widget _boostHint() { return Padding( key: const Key('home-feed-speed-boost-hint'), padding: const EdgeInsets.only(bottom: 8), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.play_arrow_rounded, color: AppColors.blue, size: 16, shadows: _boostTextShadows, ), const SizedBox(width: 4), Text( widget.l10n.speedBoosting('2.0'), style: const TextStyle( color: AppColors.blue, fontSize: 13, fontWeight: FontWeight.w700, shadows: _boostTextShadows, ), ), ], ), ); } Widget _rail() { return HomeFeedActionRail( key: const Key('home-feed-action-rail'), saved: widget.saved, liked: widget.liked, favoriteCount: widget.favoriteCount, likeCount: widget.likeCount, onSave: widget.onSave, onLike: widget.onLike, onEpisodes: widget.onEpisodes ?? widget.onOpenFullPlayer, ); } Widget _bottomInfo() { return HomeFeedInfoPanel( drama: widget.drama, episode: widget.episode, onDetail: widget.onDetail, onOpenFullPlayer: widget.onOpenFullPlayer, ); } }