Files
ReelShort/lib/pages/player/widgets/player_overlay_parts.dart
T

313 lines
8.5 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,
});
final String tooltip;
final IconData icon;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return SizedBox.square(
dimension: _topButtonSize,
child: IconButton(
tooltip: tooltip,
padding: EdgeInsets.zero,
style: IconButton.styleFrom(
backgroundColor: AppColors.controlScrim,
foregroundColor: AppColors.onPrimary,
),
onPressed: onPressed,
icon: Icon(icon, size: 18),
),
);
}
}
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.controlScrim,
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,
});
final String tooltip;
final IconData icon;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return SizedBox.square(
dimension: 40,
child: IconButton(
tooltip: tooltip,
padding: EdgeInsets.zero,
style: IconButton.styleFrom(
backgroundColor: AppColors.playerOverlayPill,
foregroundColor: AppColors.onPrimary,
),
onPressed: onPressed,
icon: Icon(icon, size: 22),
),
);
}
}
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),
),
);
}
}