Files
ReelShort/lib/pages/recommend/widgets/recommend_sections_view.dart
T

219 lines
7.2 KiB
Dart

// Author: fruit.com.cn
import 'package:flutter/material.dart';
import '../../../core/data/models/content_category.dart';
import '../../../core/data/models/drama.dart';
import '../../../core/data/services/category_controller.dart';
import '../../../core/data/services/home_feed_controller.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../core/widgets/app_pull_to_refresh.dart';
import '../../../l10n/app_localizations.dart';
import '../../home/widgets/home_drama_classification.dart';
import 'recommend_section_card.dart';
const List<String> _costumeNames = ['古装', 'costume', 'kostum'];
enum RecommendSectionKind { hits, newReleases, costume }
class RecommendSectionsView extends StatelessWidget {
const RecommendSectionsView({
super.key,
required this.feedController,
required this.categories,
required this.onOpenDrama,
required this.onMore,
});
final HomeFeedController feedController;
final CategoryController categories;
final ValueChanged<Drama> onOpenDrama;
final ValueChanged<RecommendSectionKind> onMore;
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: Listenable.merge([feedController, categories]),
builder: (context, _) {
final l10n = AppLocalizations.of(context)!;
final source = feedController.dramas;
final hits = [...source]
..sort((a, b) => b.viewCount.compareTo(a.viewCount));
final newest = [...source]
..sort((a, b) {
final left = publishedAtForDrama(a);
final right = publishedAtForDrama(b);
if (left == null && right == null) return b.id.compareTo(a.id);
if (left == null) return 1;
if (right == null) return -1;
return right.compareTo(left);
});
return AppPullToRefresh(
key: const Key('recommend-sections-refresh'),
onRefresh: () async {
await Future.wait([
feedController.refreshCurrent(),
categories.load(),
]);
},
child: ListView(
key: const Key('recommend-sections-scroll'),
physics: AppPullToRefresh.physics,
padding: const EdgeInsets.only(bottom: AppDimens.spaceXl),
children: [
_section(
context,
l10n,
key: 'hits',
title: l10n.weeklyHitsTitle,
subtitle: l10n.weeklyHitsSubtitle,
dramas: hits.take(10).toList(),
onMore: () => onMore(RecommendSectionKind.hits),
),
const SizedBox(height: AppDimens.spaceXl),
_section(
context,
l10n,
key: 'new',
title: l10n.weeklyNewTitle,
subtitle: l10n.weeklyNewSubtitle,
dramas: newest.take(10).toList(),
onMore: () => onMore(RecommendSectionKind.newReleases),
),
const SizedBox(height: AppDimens.spaceXl),
_section(
context,
l10n,
key: 'costume',
title: l10n.costumeAwardsTitle,
subtitle: l10n.costumeAwardsSubtitle,
dramas: _costumeDramas(source),
onMore: () => onMore(RecommendSectionKind.costume),
),
],
),
);
},
);
}
List<Drama> _costumeDramas(List<Drama> source) {
final category = _costumeCategory();
if (category != null) {
final fromCategory = categories.dramasOf(category.id);
if (fromCategory.isNotEmpty) return fromCategory.take(10).toList();
}
return source
.where(
(drama) =>
_containsCostume(drama.category) || _containsCostume(drama.genre),
)
.take(10)
.toList();
}
ContentCategory? _costumeCategory() {
for (final category in categories.categories) {
if (_containsCostume(category.name)) return category;
}
return null;
}
bool _containsCostume(String value) {
final normalized = value.toLowerCase();
return _costumeNames.any(normalized.contains);
}
Widget _section(
BuildContext context,
AppLocalizations l10n, {
required String key,
required String title,
required String subtitle,
required List<Drama> dramas,
required VoidCallback onMore,
}) {
return Column(
key: Key('recommend-section-$key'),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.feedPageHorizontal,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: AppDimens.recommendSectionTitle,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: const TextStyle(
color: AppColors.textSecondary,
fontSize: AppDimens.recommendSectionSubtitle,
),
),
],
),
),
TextButton(
key: Key('recommend-more-$key'),
onPressed: onMore,
style: TextButton.styleFrom(
foregroundColor: AppColors.textSecondary,
padding: const EdgeInsets.symmetric(horizontal: 4),
minimumSize: const Size(0, 30),
),
child: Row(
children: [
Text(
l10n.more,
style: const TextStyle(
color: AppColors.textSecondary,
fontSize: AppDimens.recommendSectionMore,
fontWeight: FontWeight.w500,
),
),
const Icon(Icons.chevron_right_rounded, size: 16),
],
),
),
],
),
),
const SizedBox(height: AppDimens.spaceMd),
SizedBox(
height: AppDimens.recommendRowHeight,
child: ListView.separated(
key: Key('recommend-row-$key'),
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.feedPageHorizontal,
),
itemCount: dramas.length,
separatorBuilder: (_, _) =>
const SizedBox(width: AppDimens.homeGridGap),
itemBuilder: (context, index) => RecommendSectionCard(
sectionKey: key,
drama: dramas[index],
onTap: () => onOpenDrama(dramas[index]),
),
),
),
],
);
}
}