182 lines
5.0 KiB
Dart
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,
|
|
),
|
|
);
|
|
}
|
|
}
|