Files

215 lines
6.8 KiB
Dart

// Author: fruit.com.cn
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../core/data/models/drama.dart';
import '../../core/data/services/catalog_controller.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_dimens.dart';
import '../../core/widgets/app_pull_to_refresh.dart';
import '../../core/widgets/empty_state.dart';
import '../../core/widgets/paged_local_list.dart';
import '../../core/widgets/plain_page_scaffold.dart';
import '../../l10n/app_localizations.dart';
import '../player/open_player.dart';
import 'widgets/ranking_drama_row.dart';
const int _topCount = 50;
const double _loadMoreThreshold = 200;
/// 剧场榜单页:统一行布局,名次角标压在封面左上角,右侧字段无数据不展示。
class RankingPage extends StatefulWidget {
const RankingPage({super.key, this.embedded = false});
final bool embedded;
@override
State<RankingPage> createState() => _RankingPageState();
}
class _RankingPageState extends State<RankingPage> {
final ScrollController _scroll = ScrollController();
PagedLocalListController<Drama>? _pager;
bool _refreshing = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_pager != null) return;
_pager = PagedLocalListController<Drama>(
items: _ranked(context.read<CatalogController>().dramas),
)..addListener(_rebuild);
}
@override
void dispose() {
_pager
?..removeListener(_rebuild)
..dispose();
_scroll.dispose();
super.dispose();
}
void _rebuild() {
if (mounted) setState(() {});
}
List<Drama> _ranked(List<Drama> source) {
final dramas = [...source]
..sort((a, b) => b.viewCount.compareTo(a.viewCount));
return dramas.take(_topCount).toList(growable: false);
}
Future<void> _refresh() async {
if (_refreshing) return;
_refreshing = true;
try {
final catalog = context.read<CatalogController>();
await catalog.refresh();
if (!mounted) return;
_pager!.replaceAll(_ranked(catalog.dramas));
if (_scroll.hasClients) {
_scroll.jumpTo(_scroll.position.minScrollExtent);
}
} finally {
_refreshing = false;
}
}
bool _onScroll(ScrollNotification notification) {
if (notification.depth != 0 || _refreshing) return false;
if (notification.metrics.extentAfter > _loadMoreThreshold) return false;
if (notification is ScrollUpdateNotification ||
notification is ScrollEndNotification ||
notification is OverscrollNotification) {
unawaited(_pager!.loadMore());
}
return false;
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final catalog = context.watch<CatalogController>();
final top = _pager!.visibleItems;
final content = NotificationListener<ScrollNotification>(
onNotification: _onScroll,
child: AppPullToRefresh(
key: const Key('ranking-refresh'),
onRefresh: _refresh,
child: ListView(
key: const Key('ranking-list'),
controller: _scroll,
physics: AppPullToRefresh.physics,
padding: const EdgeInsets.only(top: 8, bottom: 24),
children: top.isEmpty
? [_emptyState(l10n, catalog)]
: [
if (widget.embedded) _intro(l10n),
const SizedBox(height: AppDimens.spaceMd),
for (var index = 0; index < top.length; index++) ...[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.feedPageHorizontal,
vertical: AppDimens.spaceSm,
),
child: RankingDramaRow(
rank: index + 1,
drama: top[index],
onTap: () => unawaited(
playDramaFromCatalog(context, top[index]),
),
),
),
if (index != top.length - 1)
const Padding(
padding: EdgeInsets.symmetric(
horizontal: AppDimens.feedPageHorizontal,
),
child: Divider(
height: AppDimens.mineGridDividerWidth,
thickness: AppDimens.mineGridDividerWidth,
color: AppColors.border,
),
),
],
PagedLoadFooter(
loading: _pager!.loading,
hasMore: _pager!.hasMore,
hasItems: _pager!.visibleItems.isNotEmpty,
loadMoreKey: const Key('ranking-load-more'),
),
],
),
),
);
if (widget.embedded) {
return KeyedSubtree(key: const Key('ranking-page'), child: content);
}
return PlainPageScaffold(
pageKey: const Key('ranking-page'),
backKey: const Key('ranking-back'),
title: l10n.topCharts,
subtitle: l10n.rankingSubtitle,
child: content,
);
}
Widget _intro(AppLocalizations l10n) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.feedPageHorizontal,
),
child: Column(
key: const Key('ranking-hero'),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.topCharts,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: AppDimens.rankingHeroTitle,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
l10n.rankingSubtitle,
style: const TextStyle(
color: AppColors.textTertiary,
fontSize: AppDimens.rankingHeroSubtitle,
),
),
],
),
);
}
Widget _emptyState(AppLocalizations l10n, CatalogController catalog) {
final theme = Theme.of(context);
return SizedBox(
height: 420,
child: Theme(
data: theme.copyWith(
colorScheme: theme.colorScheme.copyWith(
onSurface: AppColors.textTertiary,
),
),
child: EmptyState(
key: Key(catalog.error == null ? 'ranking-empty' : 'ranking-error'),
icon: catalog.error == null
? Icons.leaderboard_outlined
: Icons.cloud_off_rounded,
title: catalog.error == null ? l10n.noStories : l10n.catalogError,
actionLabel: catalog.error == null ? null : l10n.retry,
onAction: catalog.error == null ? null : () => unawaited(_refresh()),
stretchContent: true,
),
),
);
}
}