feat(flutter): 接入分类记录收藏与下载授权

This commit is contained in:
Zhou Weidong committed 2026-10-17 14:49:16 +08:00
1 parent 00280ca0da
commit cc4d7c5a9d
52 files changed
+4440 -171

No files matched your search

+6 -43
View File
@@ -19,7 +19,6 @@ class DiscoverLogic extends ChangeNotifier {
final TextEditingController search = TextEditingController();
final FocusNode searchFocus = FocusNode();
final ScrollController scroll = ScrollController();
int category = 0;
bool searchOpen = false;
bool _disposed = false;
@@ -35,11 +34,6 @@ class DiscoverLogic extends ChangeNotifier {
.catchError((Object _) => null);
}
void selectCategory(int index) {
category = index;
notify();
}
void onSearchChanged() => notify();
bool get showSearch => searchOpen || search.text.isNotEmpty;
@@ -105,7 +99,12 @@ class DiscoverLogic extends ChangeNotifier {
} else if (embedded && onDramaSelected != null) {
onDramaSelected(selected);
} else {
act(context, DiscoverPageAction.detail, embedded: embedded, onAction: onAction);
act(
context,
DiscoverPageAction.detail,
embedded: embedded,
onAction: onAction,
);
}
}
@@ -123,42 +122,6 @@ class DiscoverLogic extends ChangeNotifier {
).showSnackBar(SnackBar(content: Text(l10n.demoCatalogNotice)));
}
void showFilters(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final labels = [
l10n.forYou,
l10n.hot,
l10n.newest,
l10n.romance,
l10n.actionGenre,
];
showModalBottomSheet<void>(
context: context,
backgroundColor: const Color(0xFF202126),
showDragHandle: true,
builder: (sheetContext) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(title: Text(l10n.filter)),
for (var index = 0; index < labels.length; index++)
ListTile(
title: Text(labels[index]),
trailing: index == category
? const Icon(Icons.check_rounded, color: Color(0xFF2F7BFF))
: null,
onTap: () {
selectCategory(index);
Navigator.pop(sheetContext);
},
),
const SizedBox(height: 8),
],
),
),
);
}
@override
void dispose() {
_disposed = true;
+82 -57
View File
@@ -7,7 +7,7 @@ import 'package:provider/provider.dart';
import '../../core/data/models/drama.dart';
import '../../core/data/models/home_feed.dart';
import '../../core/data/repositories/watch_history_repository.dart';
import '../../core/data/services/catalog_controller.dart';
import '../../core/data/services/category_controller.dart';
import '../../core/theme/app_colors.dart';
import '../../core/widgets/remote_image.dart';
import '../../l10n/app_localizations.dart';
@@ -96,14 +96,9 @@ class _DiscoverPageState extends State<DiscoverPage> {
Widget _page(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final catalog = context.watch<CatalogController>();
final allDramas = widget.dramas ?? [logic.drama];
final cards = _cardsFor(
allDramas,
catalog.sections,
logic.category,
'',
);
final categories = context.watch<CategoryController>();
final allDramas = [...?widget.dramas, ...categories.visibleDramas];
final cards = _cardsFor(categories.visibleDramas, '');
final content = SafeArea(
bottom: !widget.embedded,
child: Column(
@@ -118,7 +113,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
padding: const EdgeInsets.fromLTRB(16, 4, 16, 0),
child: ClipRRect(
borderRadius: BorderRadius.circular(18),
child: _carousel(l10n, catalog.banners, allDramas),
child: _carousel(l10n, categories.feed.banners, allDramas),
),
),
Padding(
@@ -126,31 +121,11 @@ class _DiscoverPageState extends State<DiscoverPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_categories(l10n),
_categories(l10n, categories),
const SizedBox(height: 18),
_continueHeader(l10n),
const SizedBox(height: 12),
if (cards.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 24),
child: Text(
l10n.noSearchResults,
style: const TextStyle(color: Color(0xFFE6E1E3)),
),
)
else
SizedBox(
height: 196,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
for (final card in cards) ...[
_continueTile(l10n, card),
const SizedBox(width: 12),
],
],
),
),
_dramaContent(l10n, categories, cards),
],
),
),
@@ -217,7 +192,9 @@ class _DiscoverPageState extends State<DiscoverPage> {
List<FeedBanner> banners,
List<Drama> allDramas,
) {
final items = banners.where((banner) => banner.imageUrl.isNotEmpty).toList();
final items = banners
.where((banner) => banner.imageUrl.isNotEmpty)
.toList();
if (items.isEmpty) return const SizedBox.shrink();
return CoverCarousel(
items: [
@@ -285,8 +262,64 @@ class _DiscoverPageState extends State<DiscoverPage> {
],
);
Widget _continueTile(AppLocalizations l10n, _ShowcaseCard card) {
final drama = logic.dramaFor(card.dramaId, widget.dramas ?? [logic.drama]);
Widget _dramaContent(
AppLocalizations l10n,
CategoryController categories,
List<_ShowcaseCard> cards,
) {
if (categories.loading && categories.feed.sections.isEmpty) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 24),
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
);
}
if (categories.error != null) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 18),
child: Row(
children: [
Expanded(
child: Text(
l10n.categoriesError,
style: const TextStyle(color: Color(0xFFE6E1E3)),
),
),
TextButton(onPressed: categories.load, child: Text(l10n.retry)),
],
),
);
}
if (cards.isEmpty) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 24),
child: Text(
categories.selectedSectionMissing
? l10n.noCategoryDramas
: l10n.noSearchResults,
style: const TextStyle(color: Color(0xFFE6E1E3)),
),
);
}
return SizedBox(
height: 196,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
for (final card in cards) ...[
_continueTile(l10n, card, categories.visibleDramas),
const SizedBox(width: 12),
],
],
),
);
}
Widget _continueTile(
AppLocalizations l10n,
_ShowcaseCard card,
List<Drama> source,
) {
final drama = logic.dramaFor(card.dramaId, source);
return SizedBox(
width: 132,
child: InkWell(
@@ -360,24 +393,25 @@ class _DiscoverPageState extends State<DiscoverPage> {
);
}
Widget _categories(AppLocalizations l10n) {
final labels = [
l10n.forYou,
l10n.hot,
l10n.newest,
l10n.romance,
l10n.actionGenre,
Widget _categories(AppLocalizations l10n, CategoryController categories) {
final choices = <({String code, String label})>[
(code: CategoryController.allCode, label: l10n.forYou),
(code: CategoryController.hotCode, label: l10n.hot),
(code: CategoryController.newCode, label: l10n.newest),
for (final category in categories.categories)
(code: 'category-${category.id}', label: category.name),
];
return SizedBox(
height: 32,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: labels.length,
itemCount: choices.length,
separatorBuilder: (_, _) => const SizedBox(width: 8),
itemBuilder: (context, index) {
final selected = logic.category == index;
final choice = choices[index];
final selected = categories.selectedCode == choice.code;
return GestureDetector(
onTap: () => logic.selectCategory(index),
onTap: () => categories.selectCode(choice.code),
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
alignment: Alignment.center,
@@ -387,7 +421,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
borderRadius: BorderRadius.circular(16),
),
child: Text(
labels[index],
choice.label,
maxLines: 1,
style: TextStyle(
color: selected ? Colors.white : AppColors.muted,
@@ -498,19 +532,10 @@ class _ShowcaseCard {
}
/// 分区由服务端下发:0 为全部,其余按分区顺序对应筛选菜单(热门 / 最新 / 各分类)。
List<_ShowcaseCard> _cardsFor(
List<Drama> dramas,
List<FeedSection> sections,
int category,
String query,
) {
final source = switch (category) {
0 => dramas,
_ => category < sections.length ? sections[category].dramas : const <Drama>[],
};
List<_ShowcaseCard> _cardsFor(List<Drama> dramas, String query) {
final normalized = query.trim().toLowerCase();
return [
for (final drama in source)
for (final drama in dramas)
if (normalized.isEmpty ||
drama.title.toLowerCase().contains(normalized) ||
drama.genre.toLowerCase().contains(normalized) ||