210 lines
5.4 KiB
Dart
210 lines
5.4 KiB
Dart
// 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.synopsis,
|
|
required this.authorNote,
|
|
});
|
|
|
|
final AppLocalizations l10n;
|
|
final Drama drama;
|
|
final String synopsis;
|
|
final String authorNote;
|
|
|
|
@override
|
|
State<_EpisodeSynopsis> createState() => _EpisodeSynopsisState();
|
|
}
|
|
|
|
class _EpisodeSynopsisState extends State<_EpisodeSynopsis> {
|
|
String? _fullSynopsis;
|
|
int _detailRequest = 0;
|
|
bool _detailRequested = false;
|
|
|
|
static const int _authorNoteLines = 2;
|
|
static const double _authorNoteGap = 4;
|
|
static const double _authorNoteFont = 10;
|
|
|
|
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;
|
|
_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 ExpandableSynopsis(
|
|
key: ValueKey(widget.drama.id),
|
|
keyPrefix: 'player',
|
|
synopsis: _synopsis,
|
|
expandedFooter: widget.authorNote.isEmpty ? null : _authorNote(),
|
|
);
|
|
}
|
|
|
|
Widget _authorNote() => Padding(
|
|
padding: const EdgeInsets.only(top: _authorNoteGap),
|
|
child: Text(
|
|
widget.authorNote,
|
|
key: const Key('player-author-note'),
|
|
maxLines: _authorNoteLines,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
color: AppColors.playerMuted,
|
|
fontSize: _authorNoteFont,
|
|
shadows: _playerTextShadows,
|
|
),
|
|
),
|
|
);
|
|
}
|