1
This commit is contained in:
@@ -69,6 +69,10 @@ class _SimplePlayerScreenState extends ConsumerState<SimplePlayerScreen> {
|
||||
static const _rates = [1.0, 1.5, 2.0, 3.0];
|
||||
double _rate = 1.0;
|
||||
|
||||
// 清屏:隐藏互动栏/信息区/顶部栏/底部渐变,仅保留播放画面与进度条。
|
||||
// 恢复只能从长按菜单进入,避免单击误触把界面弹回来
|
||||
bool _cleanMode = false;
|
||||
|
||||
bool get _hasSeries =>
|
||||
_seriesData != null && (_seriesData!['has_series'] as bool? ?? false);
|
||||
|
||||
@@ -77,6 +81,8 @@ class _SimplePlayerScreenState extends ConsumerState<SimplePlayerScreen> {
|
||||
|
||||
String get _seriesTitle =>
|
||||
_seriesData?['series']?['title'] as String? ?? '';
|
||||
String get _seriesSourceName =>
|
||||
_seriesData?['series']?['source_name'] as String? ?? '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -310,6 +316,15 @@ class _SimplePlayerScreenState extends ConsumerState<SimplePlayerScreen> {
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/// 站点管理员邮箱(长按弹窗显示「下架视频」)
|
||||
static const String _kSiteAdminEmail = '256kb@gmail.com';
|
||||
|
||||
bool get _isSiteAdmin {
|
||||
final u = ref.read(authProvider).user;
|
||||
return u != null &&
|
||||
(u.email ?? '').toLowerCase() == _kSiteAdminEmail;
|
||||
}
|
||||
|
||||
// ---------- 长按 ----------
|
||||
|
||||
void _onLongPress() {
|
||||
@@ -403,6 +418,40 @@ class _SimplePlayerScreenState extends ConsumerState<SimplePlayerScreen> {
|
||||
},
|
||||
),
|
||||
|
||||
// 清屏 / 恢复清屏:隐藏右侧互动栏与底部信息,只留播放画面与进度条。
|
||||
// 恢复只能从长按菜单进入,避免单击误触把界面弹回来
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(_cleanMode
|
||||
? Icons.fullscreen
|
||||
: Icons.fullscreen_exit),
|
||||
title: Text(_cleanMode ? '恢复清屏' : '清屏'),
|
||||
subtitle: Text(_cleanMode
|
||||
? '重新显示点赞、评论、收藏、作者与标题'
|
||||
: '隐藏点赞、评论、收藏、作者与标题'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetContext).pop();
|
||||
setState(() => _cleanMode = !_cleanMode);
|
||||
showTopToast(
|
||||
context, _cleanMode ? '已清屏,长按视频可恢复' : '已恢复显示');
|
||||
},
|
||||
),
|
||||
|
||||
// 下架视频:仅站点管理员可见(按 email 判定)
|
||||
if (_isSiteAdmin)
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.remove_circle_outline,
|
||||
color: Colors.redAccent),
|
||||
title: const Text('下架视频',
|
||||
style: TextStyle(color: Colors.redAccent)),
|
||||
subtitle: const Text('从首页/搜索/分类/系列移除该视频'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetContext).pop();
|
||||
_confirmOffline();
|
||||
},
|
||||
),
|
||||
|
||||
// 下载(已下载/下载中置灰;本地已下载视频不再重复下载)
|
||||
Builder(builder: (_) {
|
||||
final dm = ref.read(downloadManagerProvider);
|
||||
@@ -439,8 +488,47 @@ class _SimplePlayerScreenState extends ConsumerState<SimplePlayerScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- 进度条 ----------
|
||||
// ---------- 下架(管理员) ----------
|
||||
|
||||
Future<void> _confirmOffline() async {
|
||||
final targetId = _currentItem.videoId;
|
||||
final title = _currentItem.title;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('下架该视频?'),
|
||||
content: Text(
|
||||
'《$title》\n\n下架后将立刻从首页、搜索、分类、系列列表中移除,可在后台恢复。',
|
||||
maxLines: 5, overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.redAccent),
|
||||
child: const Text('下架'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !mounted) return;
|
||||
|
||||
try {
|
||||
await VideoService.offlineVideo(targetId);
|
||||
if (!mounted) return;
|
||||
showTopToast(context, '已下架');
|
||||
// 播放页停留没意义:返回上一页(触发外层列表刷新由列表自身负责)
|
||||
context.pop();
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
showTopToast(context, e is ApiException ? e.message : '下架失败,请重试');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 进度条 ----------
|
||||
/// 有效时长:流上报优先,异常(如被当直播 duration=0)回退到 DB 时长
|
||||
Duration get _effectiveDuration => _duration.inMilliseconds > 0
|
||||
? _duration
|
||||
@@ -537,16 +625,20 @@ class _SimplePlayerScreenState extends ConsumerState<SimplePlayerScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
// 底部渐变(提升文字可读性)
|
||||
const Positioned(
|
||||
// 底部渐变(提升文字可读性):清屏时一并淡出
|
||||
Positioned(
|
||||
left: 0, right: 0, bottom: 0, height: 200,
|
||||
child: IgnorePointer(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.transparent, Colors.black54],
|
||||
child: AnimatedOpacity(
|
||||
opacity: _cleanMode ? 0 : 1,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: const DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.transparent, Colors.black54],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -554,51 +646,59 @@ class _SimplePlayerScreenState extends ConsumerState<SimplePlayerScreen> {
|
||||
),
|
||||
|
||||
// ---------- 顶部:返回 + 倍速 ----------
|
||||
SafeArea(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const BackButton(color: Colors.white),
|
||||
// 倍速按钮
|
||||
GestureDetector(
|
||||
onTap: _onLongPress,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black45,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
_rateLabel,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
_HidableOverlay(
|
||||
hidden: _cleanMode,
|
||||
child: SafeArea(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const BackButton(color: Colors.white),
|
||||
// 倍速按钮
|
||||
GestureDetector(
|
||||
onTap: _onLongPress,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black45,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
_rateLabel,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ---------- 右侧互动栏 ----------
|
||||
Positioned(
|
||||
right: 8, bottom: 96,
|
||||
child: InteractionBar(
|
||||
item: interactionItem,
|
||||
onLike: (_) => _onLike(),
|
||||
onFavorite: (_) => _onFavorite(),
|
||||
onComment: (_) => _openComments(),
|
||||
child: _HidableOverlay(
|
||||
hidden: _cleanMode,
|
||||
child: InteractionBar(
|
||||
item: interactionItem,
|
||||
onLike: (_) => _onLike(),
|
||||
onFavorite: (_) => _onFavorite(),
|
||||
onComment: (_) => _openComments(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ---------- 左下信息区 ----------
|
||||
Positioned(
|
||||
left: 12, right: 88, bottom: 24,
|
||||
child: Column(
|
||||
child: _HidableOverlay(
|
||||
hidden: _cleanMode,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -628,6 +728,24 @@ class _SimplePlayerScreenState extends ConsumerState<SimplePlayerScreen> {
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (_seriesSourceName.isNotEmpty) ...[
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 5, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
_seriesSourceName,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 2),
|
||||
const Icon(Icons.chevron_right,
|
||||
color: Colors.white, size: 14),
|
||||
@@ -639,6 +757,7 @@ class _SimplePlayerScreenState extends ConsumerState<SimplePlayerScreen> {
|
||||
VideoInfoSection(item: _currentItem),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ---------- 进度条:抖音风格一条细线 + 时间刻度 ----------
|
||||
@@ -761,10 +880,33 @@ class _SimplePlayerScreenState extends ConsumerState<SimplePlayerScreen> {
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'共 ${_episodes.length} 集',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: Colors.grey[600]),
|
||||
Row(
|
||||
children: [
|
||||
if (_seriesSourceName.isNotEmpty) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 5, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
AppColors.primary.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
_seriesSourceName,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
Text(
|
||||
'共 ${_episodes.length} 集',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -992,3 +1134,24 @@ class _ProgressPainter extends CustomPainter {
|
||||
bool shouldRepaint(covariant _ProgressPainter oldDelegate) =>
|
||||
oldDelegate.value != value || oldDelegate.dragging != dragging;
|
||||
}
|
||||
|
||||
/// 可隐藏浮层:淡出 + 禁用点击。隐藏时仍占位但不拦截手势,
|
||||
/// 保证点击/长按能穿透到下层的手势层
|
||||
class _HidableOverlay extends StatelessWidget {
|
||||
const _HidableOverlay({required this.hidden, required this.child});
|
||||
|
||||
final bool hidden;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
ignoring: hidden,
|
||||
child: AnimatedOpacity(
|
||||
opacity: hidden ? 0 : 1,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user