210 lines
6.7 KiB
Dart
210 lines
6.7 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) {
|
|
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.pageHorizontal,
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: const TextStyle(
|
|
color: AppColors.textPrimary,
|
|
fontSize: AppDimens.sectionTitle,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
subtitle,
|
|
style: const TextStyle(
|
|
color: AppColors.textSecondary,
|
|
fontSize: AppDimens.cardViews,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
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(
|
|
fontSize: AppDimens.caption,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const Icon(Icons.chevron_right_rounded, size: 16),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: AppDimens.spaceMd),
|
|
SizedBox(
|
|
height: 218,
|
|
child: ListView.separated(
|
|
key: Key('recommend-row-$key'),
|
|
scrollDirection: Axis.horizontal,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: AppDimens.pageHorizontal,
|
|
),
|
|
itemCount: dramas.length,
|
|
separatorBuilder: (_, _) =>
|
|
const SizedBox(width: AppDimens.spaceMd),
|
|
itemBuilder: (context, index) => RecommendSectionCard(
|
|
sectionKey: key,
|
|
drama: dramas[index],
|
|
onTap: () => onOpenDrama(dramas[index]),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|