237 lines
7.8 KiB
Dart
237 lines
7.8 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'];
|
|
const int _sectionPreviewCount = 10;
|
|
|
|
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 = recommendSectionDramas(
|
|
kind: RecommendSectionKind.hits,
|
|
source: source,
|
|
);
|
|
final newest = recommendSectionDramas(
|
|
kind: RecommendSectionKind.newReleases,
|
|
source: source,
|
|
);
|
|
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(_sectionPreviewCount).toList(),
|
|
onMore: () => onMore(RecommendSectionKind.hits),
|
|
),
|
|
const SizedBox(height: AppDimens.spaceXl),
|
|
_section(
|
|
context,
|
|
l10n,
|
|
key: 'new',
|
|
title: l10n.weeklyNewTitle,
|
|
subtitle: l10n.weeklyNewSubtitle,
|
|
dramas: newest.take(_sectionPreviewCount).toList(),
|
|
onMore: () => onMore(RecommendSectionKind.newReleases),
|
|
),
|
|
const SizedBox(height: AppDimens.spaceXl),
|
|
_section(
|
|
context,
|
|
l10n,
|
|
key: 'costume',
|
|
title: l10n.costumeAwardsTitle,
|
|
subtitle: l10n.costumeAwardsSubtitle,
|
|
dramas: recommendSectionDramas(
|
|
kind: RecommendSectionKind.costume,
|
|
source: source,
|
|
categories: categories,
|
|
).take(_sectionPreviewCount).toList(),
|
|
onMore: () => onMore(RecommendSectionKind.costume),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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]),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 剧单分区的完整片单,横滑预览和更多页共用同一排序。
|
|
List<Drama> recommendSectionDramas({
|
|
required RecommendSectionKind kind,
|
|
required List<Drama> source,
|
|
CategoryController? categories,
|
|
}) {
|
|
switch (kind) {
|
|
case RecommendSectionKind.hits:
|
|
return [...source]..sort((a, b) => b.viewCount.compareTo(a.viewCount));
|
|
case RecommendSectionKind.newReleases:
|
|
return [...source]..sort(_newestFirst);
|
|
case RecommendSectionKind.costume:
|
|
return _costumeDramas(source, categories);
|
|
}
|
|
}
|
|
|
|
int _newestFirst(Drama leftDrama, Drama rightDrama) {
|
|
final left = publishedAtForDrama(leftDrama);
|
|
final right = publishedAtForDrama(rightDrama);
|
|
if (left == null && right == null) return rightDrama.id.compareTo(leftDrama.id);
|
|
if (left == null) return 1;
|
|
if (right == null) return -1;
|
|
return right.compareTo(left);
|
|
}
|
|
|
|
List<Drama> _costumeDramas(List<Drama> source, CategoryController? categories) {
|
|
final category = _costumeCategory(categories);
|
|
if (category != null) {
|
|
final fromCategory = categories!.dramasOf(category.id);
|
|
if (fromCategory.isNotEmpty) return fromCategory;
|
|
}
|
|
return source.where(_isCostumeDrama).toList(growable: false);
|
|
}
|
|
|
|
ContentCategory? _costumeCategory(CategoryController? categories) {
|
|
if (categories == null) return null;
|
|
for (final category in categories.categories) {
|
|
if (_containsCostume(category.name)) return category;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
bool _isCostumeDrama(Drama drama) {
|
|
return _containsCostume(drama.category) || _containsCostume(drama.genre);
|
|
}
|
|
|
|
bool _containsCostume(String value) {
|
|
final normalized = value.toLowerCase();
|
|
return _costumeNames.any(normalized.contains);
|
|
}
|