Files
ReelShort/lib/pages/drama_detail/widgets/drama_episode_states.dart
T

182 lines
5.0 KiB
Dart

// Author: fruit.com.cn
import 'package:flutter/material.dart';
import '../../../core/formatting/error_text.dart';
import '../../../core/theme/app_colors.dart';
import '../../../l10n/app_localizations.dart';
const int _skeletonCount = 6;
const int _gridColumns = 6;
const double _gridSpacing = 8;
const double _radius = 10;
const double _horizontal = 14;
const double _sectionTop = 16;
const double _minOpacity = 0.45;
const double _maxOpacity = 0.8;
const Duration _breathDuration = Duration(milliseconds: 900);
/// 详情接口返回前在选集区展示六个呼吸骨架格。
class DramaEpisodesLoading extends StatefulWidget {
const DramaEpisodesLoading({
super.key,
required this.l10n,
required this.episodeCount,
});
final AppLocalizations l10n;
final int episodeCount;
@override
State<DramaEpisodesLoading> createState() => _DramaEpisodesLoadingState();
}
class _DramaEpisodesLoadingState extends State<DramaEpisodesLoading>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _opacity;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: _breathDuration)
..repeat(reverse: true);
_opacity = Tween<double>(
begin: _minOpacity,
end: _maxOpacity,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
key: const Key('detail-episodes-section'),
padding: const EdgeInsets.fromLTRB(
_horizontal,
_sectionTop,
_horizontal,
0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
_SectionTitle(title: widget.l10n.episodes),
const Spacer(),
if (widget.episodeCount > 0)
Text(
widget.l10n.episodeCountLabel(widget.episodeCount),
style: const TextStyle(color: AppColors.muted, fontSize: 11),
),
],
),
const SizedBox(height: 8),
GridView.builder(
key: const Key('detail-episodes-loading'),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
itemCount: _skeletonCount,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _gridColumns,
mainAxisSpacing: _gridSpacing,
crossAxisSpacing: _gridSpacing,
),
itemBuilder: (context, index) => FadeTransition(
opacity: _opacity,
child: Container(
decoration: BoxDecoration(
color: AppColors.raised,
borderRadius: BorderRadius.circular(_radius),
),
),
),
),
],
),
);
}
}
/// 详情接口失败时只替换选集区,保留上方卡片与简介。
class DramaEpisodesLoadError extends StatelessWidget {
const DramaEpisodesLoadError({
super.key,
required this.l10n,
required this.message,
required this.onRetry,
});
final AppLocalizations l10n;
final String? message;
final VoidCallback onRetry;
@override
Widget build(BuildContext context) {
return Padding(
key: const Key('detail-episodes-section'),
padding: const EdgeInsets.fromLTRB(
_horizontal,
_sectionTop,
_horizontal,
0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SectionTitle(title: l10n.episodes),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.card,
borderRadius: BorderRadius.circular(14),
),
child: Column(
children: [
Text(
errorText(l10n, message, l10n.detailError),
textAlign: TextAlign.center,
style: const TextStyle(color: AppColors.muted, fontSize: 13),
),
const SizedBox(height: 10),
TextButton(
key: const Key('detail-episodes-retry'),
onPressed: onRetry,
child: Text(l10n.retry),
),
],
),
),
],
),
);
}
}
class _SectionTitle extends StatelessWidget {
const _SectionTitle({required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Text(
title,
style: const TextStyle(
color: AppColors.onSurface,
fontSize: 14,
fontWeight: FontWeight.w800,
),
);
}
}