Files
ReelShort/lib/pages/following/following_favorites_grid.dart
T

206 lines
7.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<bool> editing;
final ValueListenable<Set<String>> selected;
final ValueChanged<String> 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<WatchRecord> onContinueRecord;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: selection.editing,
builder: (context, isEditing, _) {
return ValueListenableBuilder<Set<String>>(
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<String> 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<Widget> _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<String> 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));
}
}