Files
ReelShort/lib/pages/player/widgets/player_overlay_parts.dart
T
Vicky 543d0aadd5 style(player): 播放页与首页浮层的控件样式统一
播放页顶部按钮与选集条去掉底色、进度条不再画滑块圆点并压到最底部;
剧名与简介收在互动栏左侧,清屏后只留右下角退出按钮。
首页观看条与进度条共用 20pt 页边距,进度槽底色与播放页一致。
2026-10-29 15:44:22 +08:00

323 lines
8.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Author: fruit.com.cn
part of 'player_overlay.dart';
/// 顶部按钮:直接压在画面上,不加底色,靠文字/图标阴影保证可读性。
class _TopButton extends StatelessWidget {
const _TopButton({
super.key,
required this.tooltip,
required this.icon,
required this.onPressed,
this.iconSize = 22,
});
final String tooltip;
final IconData icon;
final VoidCallback onPressed;
final double iconSize;
@override
Widget build(BuildContext context) {
return SizedBox.square(
dimension: _topButtonSize,
child: IconButton(
tooltip: tooltip,
padding: EdgeInsets.zero,
style: IconButton.styleFrom(
backgroundColor: AppColors.transparent,
foregroundColor: AppColors.onPrimary,
),
onPressed: onPressed,
icon: Icon(icon, size: iconSize, shadows: _playerTextShadows),
),
);
}
}
class _SpeedEntry extends StatelessWidget {
const _SpeedEntry({super.key, required this.speed, required this.onTap});
final double speed;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Material(
color: AppColors.transparent,
borderRadius: BorderRadius.circular(999),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(999),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.speed_rounded,
color: AppColors.onPrimary,
size: 15,
shadows: _playerTextShadows,
),
const SizedBox(width: 4),
Text(
'${speed}x',
style: const TextStyle(
color: AppColors.onPrimary,
fontSize: 12,
fontWeight: FontWeight.w700,
shadows: _playerTextShadows,
),
),
],
),
),
),
);
}
}
class _BottomIconButton extends StatelessWidget {
const _BottomIconButton({
super.key,
required this.tooltip,
required this.icon,
required this.onPressed,
this.height = 40,
});
final String tooltip;
final IconData icon;
final VoidCallback onPressed;
/// 与左侧选集条等高,两个控件底部齐平。
final double height;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 40,
height: height,
child: IconButton(
tooltip: tooltip,
padding: EdgeInsets.zero,
style: IconButton.styleFrom(
backgroundColor: AppColors.transparent,
foregroundColor: AppColors.onPrimary,
),
onPressed: onPressed,
icon: Icon(icon, size: 24, shadows: _playerTextShadows),
),
);
}
}
class _EpisodeSynopsis extends StatefulWidget {
const _EpisodeSynopsis({
super.key,
required this.l10n,
required this.drama,
required this.episodeNumber,
required this.synopsis,
required this.authorNote,
});
final AppLocalizations l10n;
final Drama drama;
final int episodeNumber;
final String synopsis;
final String authorNote;
@override
State<_EpisodeSynopsis> createState() => _EpisodeSynopsisState();
}
class _EpisodeSynopsisState extends State<_EpisodeSynopsis> {
bool _expanded = false;
String? _fullSynopsis;
int _detailRequest = 0;
bool _detailRequested = false;
/// 「第 n 集」、分隔符与简介正文共用同一套白字规格。
static const TextStyle _lineStyle = TextStyle(
color: AppColors.onPrimary,
fontSize: 12,
fontWeight: FontWeight.w500,
height: 1.35,
shadows: _playerTextShadows,
);
static const TextStyle _linkStyle = TextStyle(
color: AppColors.textPrimary,
fontSize: 12,
fontWeight: FontWeight.w600,
shadows: _playerTextShadows,
);
static const int _collapsedMaxLines = 2;
String get _synopsis => _fullSynopsis ?? widget.synopsis;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_detailRequested || widget.drama.detailLoaded) return;
_detailRequested = true;
unawaited(_loadFullSynopsis());
}
@override
void didUpdateWidget(covariant _EpisodeSynopsis oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.drama.id == widget.drama.id &&
oldWidget.synopsis == widget.synopsis) {
return;
}
_fullSynopsis = null;
_expanded = false;
_detailRequested = !widget.drama.detailLoaded;
if (_detailRequested) unawaited(_loadFullSynopsis());
}
Future<void> _loadFullSynopsis() async {
if (widget.drama.detailLoaded) return;
final request = ++_detailRequest;
final CatalogController catalog;
try {
catalog = context.read<CatalogController>();
} catch (_) {
return;
}
try {
final detail = await catalog.requireDetail(widget.drama);
if (!mounted || request != _detailRequest) return;
final full = localizedDramaSynopsis(widget.l10n, detail);
if (full == widget.synopsis) return;
setState(() => _fullSynopsis = full);
} catch (_) {
// 详情失败时继续用列表里的简介。
}
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) => _block(constraints.maxWidth),
);
}
/// 与首页推荐 feed 的简介展开/收起保持一致:
/// 折叠单行省略 + 行尾「展开」(整行可点);展开后内联「收起」。
Widget _block(double maxWidth) {
final label = widget.l10n.episodeLabel(widget.episodeNumber);
final span = TextSpan(
style: _lineStyle,
children: [
TextSpan(text: label, style: _lineStyle),
_separatorSpan(),
TextSpan(text: _synopsis, style: _lineStyle),
],
);
if (_expanded) return _expandedBlock(span);
final painter = TextPainter(
text: span,
maxLines: 1,
textDirection: Directionality.of(context),
textScaler: MediaQuery.textScalerOf(context),
)..layout(maxWidth: maxWidth);
if (!painter.didExceedMaxLines) {
return Text.rich(
span,
key: const Key('player-synopsis'),
maxLines: 1,
style: _lineStyle,
);
}
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => setState(() => _expanded = true),
child: Row(
key: const Key('player-synopsis-collapsed'),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text.rich(
span,
key: const Key('player-synopsis'),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: _lineStyle,
),
),
const SizedBox(width: 2),
_toggle(
widget.l10n.expandSynopsis,
padding: const EdgeInsets.only(left: 6),
),
],
),
);
}
Widget _expandedBlock(InlineSpan span) {
final expandedSpan = TextSpan(
style: _lineStyle,
children: [
span,
WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
child: _toggle(
widget.l10n.showLess,
padding: const EdgeInsets.only(left: 8),
),
),
],
);
return Column(
key: const Key('player-synopsis-expanded'),
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text.rich(expandedSpan, key: const Key('player-synopsis')),
if (widget.authorNote.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
widget.authorNote,
key: const Key('player-author-note'),
maxLines: _collapsedMaxLines,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.playerMuted,
fontSize: 10,
shadows: _playerTextShadows,
),
),
],
],
);
}
/// 分隔竖线:同字号 w400 + 45% 透明白,靠两侧空格留白。
/// 必须用 TextSpan:WidgetSpan 占位盒的基线对齐会让第n集与简介错位。
TextSpan _separatorSpan() => TextSpan(
text: ' | ',
style: _lineStyle.copyWith(
color: AppColors.onPrimary.withValues(alpha: 0.45),
fontWeight: FontWeight.w400,
),
);
Widget _toggle(String label, {required EdgeInsets padding}) {
return GestureDetector(
key: const Key('player-synopsis-toggle'),
behavior: HitTestBehavior.opaque,
onTap: () => setState(() => _expanded = !_expanded),
child: Padding(
padding: padding,
child: Text(label, maxLines: 1, style: _linkStyle),
),
);
}
}