// Author: fruit.com.cn import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../../core/data/models/watch_record.dart'; import '../../core/data/services/favorite_controller.dart'; import '../../core/data/services/watch_records_controller.dart'; import '../../core/formatting/error_text.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_dimens.dart'; import '../../core/widgets/empty_state.dart'; import '../../l10n/app_localizations.dart'; import '../player/open_player.dart'; import 'following_grid_card.dart'; import 'following_paged_grid.dart'; import 'widgets/continue_watching_banner.dart'; /// 追剧编辑中的选中状态。 class FollowingFavoriteSelection { const FollowingFavoriteSelection({ required this.editing, required this.selected, required this.onToggle, }); final ValueListenable editing; final ValueListenable> selected; final ValueChanged onToggle; } /// 追剧列表。编辑时点卡片只切换选中,不进入播放。 class FollowingFavoritesGrid extends StatelessWidget { const FollowingFavoritesGrid({ super.key, required this.controller, required this.history, required this.selection, required this.scrollController, required this.onGoHome, required this.onContinueRecord, }); final FavoriteController controller; final WatchRecordsController history; final FollowingFavoriteSelection selection; final ScrollController scrollController; final VoidCallback onGoHome; final ValueChanged onContinueRecord; @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: selection.editing, builder: (context, isEditing, _) { return ValueListenableBuilder>( valueListenable: selection.selected, builder: (context, picked, _) { return ListenableBuilder( listenable: Listenable.merge([controller, history]), builder: (context, _) => _grid(context, isEditing, picked), ); }, ); }, ); } Widget _grid(BuildContext context, bool isEditing, Set picked) { final l10n = AppLocalizations.of(context)!; final hasFavorites = controller.cards.isNotEmpty; final extra = !isEditing && hasFavorites ? 1 : 0; return FollowingPagedGrid( key: const Key('following-favorites-grid'), scrollController: scrollController, loading: controller.loading, loadingMore: controller.loadingMore, hasMore: controller.hasMore, hasItems: hasFavorites, showInitialLoading: false, errorMessage: controller.error == null ? null : errorText(l10n, controller.error, l10n.favoritesError), loadMoreError: controller.loadMoreError == null ? null : errorText(l10n, controller.loadMoreError, l10n.favoritesError), onRefresh: controller.load, onLoadMore: controller.loadMore, itemCount: controller.cards.length + extra, topSlivers: _topSlivers(context), empty: EmptyState( key: const Key('following-favorites-empty'), icon: Icons.bookmark_border_rounded, title: l10n.noFavoritesYet, ), itemBuilder: (context, index) => _item(context, index, isEditing, picked), ); } List _topSlivers(BuildContext context) { final l10n = AppLocalizations.of(context)!; final continueRecord = _continueRecord(); return [ if (continueRecord != null) SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB( AppDimens.feedPageHorizontal, 4, AppDimens.feedPageHorizontal, 0, ), child: ContinueWatchingBanner( record: continueRecord, languageCode: Localizations.localeOf(context).languageCode, onTap: () => onContinueRecord(continueRecord), ), ), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB( AppDimens.feedPageHorizontal, AppDimens.followingSectionTopGap, AppDimens.feedPageHorizontal, AppDimens.followingSectionBottomGap, ), child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( l10n.favoriteDramas, style: const TextStyle( color: AppColors.textPrimary, fontSize: AppDimens.followingSectionTitleFont, fontWeight: FontWeight.w400, ), ), const Spacer(), Padding( padding: const EdgeInsets.only(bottom: 2), child: Text( l10n.totalDramasCount(controller.total), style: const TextStyle( color: AppColors.textTabInactive, fontSize: AppDimens.followingSectionCountFont, ), ), ), ], ), ), ), ]; } /// 最近一条观看记录(records 已按最近观看排序,与观看历史页首条一致)。 WatchRecord? _continueRecord() { return history.records.isEmpty ? null : history.records.first; } Widget _item( BuildContext context, int index, bool isEditing, Set picked, ) { final l10n = AppLocalizations.of(context)!; if (index == controller.cards.length) { return FollowingAddCard(onTap: onGoHome); } final drama = controller.cards[index]; final record = history.recordForDrama(drama.id); final episode = drama.episodes.isEmpty ? 1 : drama.episodes.first.number; final watchedEpisode = record?.episodeNumber ?? 0; final total = drama.episodeCount; final subtitle = watchedEpisode > 0 && total > 0 ? l10n.watchedEpisodeTotal(watchedEpisode, total) : l10n.favoriteEpisodeProgress(episode, total); return FollowingGridCard( dramaId: drama.id, title: drama.localizedTitle(Localizations.localeOf(context).languageCode), posterUrl: drama.posterUrl, viewCount: drama.viewCount, editing: isEditing, selected: isEditing ? picked.contains(drama.id) : null, subtitle: subtitle, // TODO(backend): Drama 增加 hasUpdate / latestEpisodeNumber / isFinished // 字段后,在此传 updateBadge(l10n.hasUpdateBadge)、红色更新副标题 // (l10n.updatedToEpisode)或灰色完结副标题(l10n.finishedAllEpisodes)。 onTap: () => _openOrToggle(context, drama.id, isEditing), ); } void _openOrToggle(BuildContext context, String dramaId, bool isEditing) { if (isEditing) { selection.onToggle(dramaId); return; } final drama = controller.cards.firstWhere((item) => item.id == dramaId); unawaited(playDramaFromCatalog(context, drama)); } }