1
This commit is contained in:
@@ -8,6 +8,8 @@ import 'package:media_kit_video/media_kit_video.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import '../../core/config/test_stream.dart';
|
||||
import '../../core/media/download_manager.dart';
|
||||
import '../../core/media/local_media_server.dart';
|
||||
import '../../core/network/api_exception.dart';
|
||||
import '../../core/storage/watch_dedup_store.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
@@ -100,7 +102,9 @@ class _SimplePlayerScreenState extends ConsumerState<SimplePlayerScreen> {
|
||||
}
|
||||
|
||||
void _open(VideoItem item) {
|
||||
_player.open(Media(resolvePlayUrl(item)), play: true);
|
||||
// 本地已下载地址(127.0.0.1)原样播放;网络 m3u8 经本地代理走 1G 分片缓存
|
||||
_player.open(Media(LocalMediaServer.I.wrap(resolvePlayUrl(item))),
|
||||
play: true);
|
||||
}
|
||||
|
||||
void _toggle() {
|
||||
@@ -317,7 +321,10 @@ class _SimplePlayerScreenState extends ConsumerState<SimplePlayerScreen> {
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (sheetContext) => Padding(
|
||||
builder: (sheetContext) => Theme(
|
||||
// 弹窗是白底,APP 主题为暗色(默认白字),需显式套用亮色主题
|
||||
data: ThemeData.light(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -395,21 +402,64 @@ class _SimplePlayerScreenState extends ConsumerState<SimplePlayerScreen> {
|
||||
_share();
|
||||
},
|
||||
),
|
||||
|
||||
// 下载(已下载/下载中置灰;本地已下载视频不再重复下载)
|
||||
Builder(builder: (_) {
|
||||
final dm = ref.read(downloadManagerProvider);
|
||||
final downloaded =
|
||||
dm.items.any((e) => e.videoId == _currentItem.videoId);
|
||||
final downloading =
|
||||
dm.progress.containsKey(_currentItem.videoId);
|
||||
final isLocal = _currentItem.videoUrl.contains('127.0.0.1');
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(downloaded || isLocal
|
||||
? Icons.check_circle_outline
|
||||
: Icons.download_outlined),
|
||||
title: Text(downloaded || isLocal
|
||||
? '已下载'
|
||||
: (downloading ? '下载中…' : '下载')),
|
||||
enabled: !downloaded && !downloading && !isLocal,
|
||||
onTap: () {
|
||||
Navigator.of(sheetContext).pop();
|
||||
showTopToast(context, '开始下载,可在「我的 → 已下载」查看');
|
||||
ref
|
||||
.read(downloadManagerProvider.notifier)
|
||||
.start(_currentItem)
|
||||
.catchError((_) {
|
||||
if (mounted) showTopToast(context, '下载失败,请重试');
|
||||
});
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- 进度条 ----------
|
||||
|
||||
/// 有效时长:流上报优先,异常(如被当直播 duration=0)回退到 DB 时长
|
||||
Duration get _effectiveDuration => _duration.inMilliseconds > 0
|
||||
? _duration
|
||||
: Duration(seconds: _currentItem.duration);
|
||||
|
||||
double get _sliderValue {
|
||||
if (_seeking) return _seekValue;
|
||||
if (_duration.inMilliseconds <= 0) return 0;
|
||||
return (_position.inMilliseconds / _duration.inMilliseconds)
|
||||
if (_effectiveDuration.inMilliseconds <= 0) return 0;
|
||||
return (_position.inMilliseconds / _effectiveDuration.inMilliseconds)
|
||||
.clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
static String _fmtDuration(Duration d) {
|
||||
final h = d.inHours;
|
||||
final m = d.inMinutes.remainder(60).toString().padLeft(2, '0');
|
||||
final s = d.inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||
return h > 0 ? '$h:$m:$s' : '$m:$s';
|
||||
}
|
||||
|
||||
void _onSeekStart(double v) {
|
||||
setState(() {
|
||||
_seeking = true;
|
||||
@@ -423,9 +473,9 @@ class _SimplePlayerScreenState extends ConsumerState<SimplePlayerScreen> {
|
||||
|
||||
void _onSeekEnd(double v) {
|
||||
setState(() => _seeking = false);
|
||||
if (_duration.inMilliseconds > 0) {
|
||||
if (_effectiveDuration.inMilliseconds > 0) {
|
||||
final target = Duration(
|
||||
milliseconds: (v * _duration.inMilliseconds).round(),
|
||||
milliseconds: (v * _effectiveDuration.inMilliseconds).round(),
|
||||
);
|
||||
_player.seek(target);
|
||||
}
|
||||
@@ -591,14 +641,45 @@ class _SimplePlayerScreenState extends ConsumerState<SimplePlayerScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
// ---------- 进度条:抖音风格一条细线 ----------
|
||||
// ---------- 进度条:抖音风格一条细线 + 时间刻度 ----------
|
||||
Positioned(
|
||||
left: 0, right: 0, bottom: 0,
|
||||
child: _ThinProgressBar(
|
||||
value: _sliderValue,
|
||||
onChanged: _onSeek,
|
||||
onChangeStart: _onSeekStart,
|
||||
onChangeEnd: _onSeekEnd,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 时间刻度:左当前(拖动时为目标位置),右总时长
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 14, vertical: 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_fmtDuration(_seeking
|
||||
? Duration(
|
||||
milliseconds: (_seekValue *
|
||||
_effectiveDuration.inMilliseconds)
|
||||
.round())
|
||||
: _position),
|
||||
style: const TextStyle(
|
||||
color: Colors.white70, fontSize: 11),
|
||||
),
|
||||
Text(
|
||||
_fmtDuration(_effectiveDuration),
|
||||
style: const TextStyle(
|
||||
color: Colors.white70, fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_ThinProgressBar(
|
||||
value: _sliderValue,
|
||||
onChanged: _onSeek,
|
||||
onChangeStart: _onSeekStart,
|
||||
onChangeEnd: _onSeekEnd,
|
||||
totalMs: _effectiveDuration.inMilliseconds,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -648,7 +729,9 @@ class _SimplePlayerScreenState extends ConsumerState<SimplePlayerScreen> {
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (_) => DraggableScrollableSheet(
|
||||
builder: (_) => Theme(
|
||||
data: ThemeData.light(),
|
||||
child: DraggableScrollableSheet(
|
||||
initialChildSize: 0.6,
|
||||
minChildSize: 0.4,
|
||||
maxChildSize: 0.9,
|
||||
@@ -744,6 +827,7 @@ class _SimplePlayerScreenState extends ConsumerState<SimplePlayerScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -757,6 +841,7 @@ class _ThinProgressBar extends StatefulWidget {
|
||||
required this.onChanged,
|
||||
required this.onChangeStart,
|
||||
required this.onChangeEnd,
|
||||
this.totalMs = 0,
|
||||
});
|
||||
|
||||
final double value;
|
||||
@@ -764,10 +849,22 @@ class _ThinProgressBar extends StatefulWidget {
|
||||
final ValueChanged<double> onChangeStart;
|
||||
final ValueChanged<double> onChangeEnd;
|
||||
|
||||
/// 视频总时长(毫秒):拖动时气泡显示目标位置用时/分/秒
|
||||
final int totalMs;
|
||||
|
||||
@override
|
||||
State<_ThinProgressBar> createState() => _ThinProgressBarState();
|
||||
}
|
||||
|
||||
/// 拖动气泡时间:>1 小时显示 x时xx分xx秒,否则 xx分xx秒
|
||||
String _seekBubbleText(double value, int totalMs) {
|
||||
final d = Duration(milliseconds: (value * totalMs).round());
|
||||
final h = d.inHours;
|
||||
final m = d.inMinutes.remainder(60).toString().padLeft(2, '0');
|
||||
final s = d.inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||
return h > 0 ? '$h时$m分$s秒' : '$m分$s秒';
|
||||
}
|
||||
|
||||
class _ThinProgressBarState extends State<_ThinProgressBar> {
|
||||
bool _dragging = false;
|
||||
double _dragValue = 0;
|
||||
@@ -805,19 +902,48 @@ class _ThinProgressBarState extends State<_ThinProgressBar> {
|
||||
_start(d.localPosition, w);
|
||||
},
|
||||
onTapUp: (_) => _end(w),
|
||||
child: Container(
|
||||
child: SizedBox(
|
||||
height: 32, // 触摸热区 32px(比视觉线条大,提升交互)
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
height: _dragging ? 4 : 2,
|
||||
width: double.infinity,
|
||||
child: CustomPaint(
|
||||
painter: _ProgressPainter(
|
||||
value: _dragging ? _dragValue : widget.value,
|
||||
dragging: _dragging,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
height: _dragging ? 4 : 2,
|
||||
width: double.infinity,
|
||||
child: CustomPaint(
|
||||
painter: _ProgressPainter(
|
||||
value: _dragging ? _dragValue : widget.value,
|
||||
dragging: _dragging,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 拖动中的时间气泡(跟随 thumb,两端自动夹在屏内)
|
||||
if (_dragging && widget.totalMs > 0)
|
||||
Positioned(
|
||||
bottom: 14,
|
||||
left: (_dragValue * w - 38)
|
||||
.clamp(0.0, (w - 76).clamp(0.0, w)),
|
||||
child: Container(
|
||||
width: 76,
|
||||
height: 24,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.82),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
_seekBubbleText(_dragValue, widget.totalMs),
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 11.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user