merge: 优化短剧列表下拉刷新体验
This commit is contained in:
commit
4291031ace
8 files changed
+247
-201
No files matched your search
@@ -1124,3 +1124,10 @@ $ flutter test
|
|||||||
- 播放量贴封面右下角(底 2、右 6),火焰图标 11 + 12/w400 纯文字,间距 4,并保留极淡底部渐变。
|
- 播放量贴封面右下角(底 2、右 6),火焰图标 11 + 12/w400 纯文字,间距 4,并保留极淡底部渐变。
|
||||||
- 修复二级 chip 运行时颜色误用 `const TextStyle` 的编译错误,并校正相关测试 Finder 类型。
|
- 修复二级 chip 运行时颜色误用 `const TextStyle` 的编译错误,并校正相关测试 Finder 类型。
|
||||||
- 全部短剧的地区、字幕、配音筛选 UI 已占位;以下接口待补:地区、字幕/配音筛选数据。
|
- 全部短剧的地区、字幕、配音筛选 UI 已占位;以下接口待补:地区、字幕/配音筛选数据。
|
||||||
|
|
||||||
|
## 2026-10-22 短剧列表刷新体验优化
|
||||||
|
|
||||||
|
- 公共下拉刷新统一为白指示器、40 displacement、Bouncing + AlwaysScrollable,并在下拉期间屏蔽内容点击与滚动条。
|
||||||
|
- 首页移除父级 feed 监听,网格、剧单和全部短剧改为局部监听,刷新时保留旧数据及原滚动位置。
|
||||||
|
- 剧单、全部短剧、追剧沿用相同刷新策略;首帧加载与刷新指示器分离。
|
||||||
|
- 按当前流程未执行 analyze/test。
|
||||||
@@ -10,7 +10,7 @@ import '../theme/app_colors.dart';
|
|||||||
const double _maxPullDistance = 96;
|
const double _maxPullDistance = 96;
|
||||||
const double _refreshThreshold = 56;
|
const double _refreshThreshold = 56;
|
||||||
const double _indicatorHeight = 56;
|
const double _indicatorHeight = 56;
|
||||||
const double _indicatorMaxOffset = 10;
|
const double _indicatorDisplacement = 40;
|
||||||
const Duration _arrowTurnDuration = Duration(milliseconds: 160);
|
const Duration _arrowTurnDuration = Duration(milliseconds: 160);
|
||||||
const Duration _settleDuration = Duration(milliseconds: 200);
|
const Duration _settleDuration = Duration(milliseconds: 200);
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ class AppPullToRefresh extends StatefulWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
static const AlwaysScrollableScrollPhysics physics =
|
static const AlwaysScrollableScrollPhysics physics =
|
||||||
AlwaysScrollableScrollPhysics(parent: ClampingScrollPhysics());
|
AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics());
|
||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final Future<void> Function() onRefresh;
|
final Future<void> Function() onRefresh;
|
||||||
@@ -146,10 +146,13 @@ class _AppPullToRefreshState extends State<AppPullToRefresh>
|
|||||||
onNotification: _onScroll,
|
onNotification: _onScroll,
|
||||||
child: AnimatedBuilder(
|
child: AnimatedBuilder(
|
||||||
animation: _offsetController,
|
animation: _offsetController,
|
||||||
child: Listener(
|
child: ScrollConfiguration(
|
||||||
onPointerUp: (_) => _release(),
|
behavior: const _NoScrollbarScrollBehavior(),
|
||||||
onPointerCancel: (_) => _release(),
|
child: Listener(
|
||||||
child: widget.child,
|
onPointerUp: (_) => _release(),
|
||||||
|
onPointerCancel: (_) => _release(),
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
final offset = _offsetController.value;
|
final offset = _offsetController.value;
|
||||||
@@ -160,7 +163,7 @@ class _AppPullToRefreshState extends State<AppPullToRefresh>
|
|||||||
key: const Key('pull-refresh-content'),
|
key: const Key('pull-refresh-content'),
|
||||||
offset: Offset(0, offset),
|
offset: Offset(0, offset),
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
ignoring: _refreshing || _retracting,
|
ignoring: offset > 0 || _refreshing || _retracting,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -185,7 +188,7 @@ class _AppPullToRefreshState extends State<AppPullToRefresh>
|
|||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: progress,
|
opacity: progress,
|
||||||
child: Transform.translate(
|
child: Transform.translate(
|
||||||
offset: Offset(0, _indicatorMaxOffset * progress),
|
offset: Offset(0, _indicatorDisplacement * progress),
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -206,7 +209,7 @@ class _AppPullToRefreshState extends State<AppPullToRefresh>
|
|||||||
dimension: 18,
|
dimension: 18,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
color: AppColors.blue,
|
color: AppColors.navSelected,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
l10n.refreshingLabel,
|
l10n.refreshingLabel,
|
||||||
@@ -220,7 +223,7 @@ class _AppPullToRefreshState extends State<AppPullToRefresh>
|
|||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.arrow_downward_rounded,
|
Icons.arrow_downward_rounded,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: AppColors.blue,
|
color: AppColors.navSelected,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
armed ? l10n.releaseToRefresh : l10n.pullToRefresh,
|
armed ? l10n.releaseToRefresh : l10n.pullToRefresh,
|
||||||
@@ -236,7 +239,7 @@ class _AppPullToRefreshState extends State<AppPullToRefresh>
|
|||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.blue,
|
color: AppColors.navSelected,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
@@ -245,3 +248,14 @@ class _AppPullToRefreshState extends State<AppPullToRefresh>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _NoScrollbarScrollBehavior extends MaterialScrollBehavior {
|
||||||
|
const _NoScrollbarScrollBehavior();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildScrollbar(
|
||||||
|
BuildContext context,
|
||||||
|
Widget child,
|
||||||
|
ScrollableDetails details,
|
||||||
|
) => child;
|
||||||
|
}
|
||||||
@@ -210,20 +210,14 @@ class _HomeTabsState extends State<_HomeTabs> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
logic = HomeBinding().create(widget.feedController.dramas);
|
logic = HomeBinding().create(widget.feedController.dramas);
|
||||||
widget.feedController.addListener(_onFeedChanged);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
widget.feedController.removeListener(_onFeedChanged);
|
|
||||||
logic.dispose();
|
logic.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onFeedChanged() {
|
|
||||||
if (mounted) setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
|
|||||||
@@ -152,83 +152,90 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
|
|||||||
required HomePrimaryTab tab,
|
required HomePrimaryTab tab,
|
||||||
required List<HomeGridCategoryShortcut> shortcuts,
|
required List<HomeGridCategoryShortcut> shortcuts,
|
||||||
}) {
|
}) {
|
||||||
final tag = _selectedTags[tab];
|
return ListenableBuilder(
|
||||||
final dramas = _dramasFor(tab, tag, shortcuts);
|
listenable: widget.feedController,
|
||||||
return LayoutBuilder(
|
builder: (context, _) {
|
||||||
builder: (context, constraints) {
|
if (widget.feedController.loadingFirstPage) {
|
||||||
final itemWidth =
|
return const Center(
|
||||||
(constraints.maxWidth -
|
child: SizedBox.square(
|
||||||
AppDimens.feedPageHorizontal * 2 -
|
dimension: 24,
|
||||||
AppDimens.homeGridGap * 2) /
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
3;
|
|
||||||
final itemExtent =
|
|
||||||
itemWidth / AppDimens.cardCoverAspectRatio +
|
|
||||||
AppDimens.homeCardBodyExtent;
|
|
||||||
return NotificationListener<ScrollNotification>(
|
|
||||||
onNotification: _onScroll,
|
|
||||||
child: AppPullToRefresh(
|
|
||||||
key: const Key('home-grid-refresh'),
|
|
||||||
onRefresh: _refresh,
|
|
||||||
child: CustomScrollView(
|
|
||||||
key: const Key('home-grid-scroll'),
|
|
||||||
controller: _scrollControllers[tab],
|
|
||||||
physics: AppPullToRefresh.physics,
|
|
||||||
slivers: [
|
|
||||||
if (widget.feedController.loadingFirstPage)
|
|
||||||
const SliverFillRemaining(
|
|
||||||
hasScrollBody: false,
|
|
||||||
child: Center(
|
|
||||||
child: SizedBox.square(
|
|
||||||
dimension: 24,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else if (dramas.isEmpty)
|
|
||||||
SliverFillRemaining(
|
|
||||||
hasScrollBody: false,
|
|
||||||
child: _emptyState(l10n, tab, tag),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
SliverPadding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: AppDimens.feedPageHorizontal,
|
|
||||||
),
|
|
||||||
sliver: SliverGrid(
|
|
||||||
key: const Key('home-grid-cards'),
|
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: 3,
|
|
||||||
crossAxisSpacing: AppDimens.homeGridGap,
|
|
||||||
mainAxisSpacing: AppDimens.homeGridRowGap,
|
|
||||||
mainAxisExtent: itemExtent,
|
|
||||||
),
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(context, index) => HomeFeedCard(
|
|
||||||
drama: dramas[index],
|
|
||||||
onTap: () => widget.onOpenDrama(dramas[index]),
|
|
||||||
),
|
|
||||||
childCount: dramas.length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (dramas.isNotEmpty)
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: AppDimens.spaceXl),
|
|
||||||
child: PagedListFooter(
|
|
||||||
loading: widget.feedController.loadingMore,
|
|
||||||
hasMore: widget.feedController.hasMore,
|
|
||||||
hasItems: dramas.isNotEmpty,
|
|
||||||
errorMessage: widget.feedController.lastError == null
|
|
||||||
? null
|
|
||||||
: l10n.catalogError,
|
|
||||||
onRetry: widget.feedController.loadMore,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
}
|
||||||
|
final tag = _selectedTags[tab];
|
||||||
|
final dramas = _dramasFor(tab, tag, shortcuts);
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final itemWidth =
|
||||||
|
(constraints.maxWidth -
|
||||||
|
AppDimens.feedPageHorizontal * 2 -
|
||||||
|
AppDimens.homeGridGap * 2) /
|
||||||
|
3;
|
||||||
|
final itemExtent =
|
||||||
|
itemWidth / AppDimens.cardCoverAspectRatio +
|
||||||
|
AppDimens.homeCardBodyExtent;
|
||||||
|
return NotificationListener<ScrollNotification>(
|
||||||
|
onNotification: _onScroll,
|
||||||
|
child: AppPullToRefresh(
|
||||||
|
key: const Key('home-grid-refresh'),
|
||||||
|
onRefresh: _refresh,
|
||||||
|
child: CustomScrollView(
|
||||||
|
key: const Key('home-grid-scroll'),
|
||||||
|
controller: _scrollControllers[tab],
|
||||||
|
physics: AppPullToRefresh.physics,
|
||||||
|
slivers: [
|
||||||
|
if (dramas.isEmpty)
|
||||||
|
SliverFillRemaining(
|
||||||
|
hasScrollBody: false,
|
||||||
|
child: _emptyState(l10n, tab, tag),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: AppDimens.feedPageHorizontal,
|
||||||
|
),
|
||||||
|
sliver: SliverGrid(
|
||||||
|
key: const Key('home-grid-cards'),
|
||||||
|
gridDelegate:
|
||||||
|
SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 3,
|
||||||
|
crossAxisSpacing: AppDimens.homeGridGap,
|
||||||
|
mainAxisSpacing: AppDimens.homeGridRowGap,
|
||||||
|
mainAxisExtent: itemExtent,
|
||||||
|
),
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) => HomeFeedCard(
|
||||||
|
drama: dramas[index],
|
||||||
|
onTap: () => widget.onOpenDrama(dramas[index]),
|
||||||
|
),
|
||||||
|
childCount: dramas.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (dramas.isNotEmpty)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
bottom: AppDimens.spaceXl,
|
||||||
|
),
|
||||||
|
child: PagedListFooter(
|
||||||
|
loading: widget.feedController.loadingMore,
|
||||||
|
hasMore: widget.feedController.hasMore,
|
||||||
|
hasItems: dramas.isNotEmpty,
|
||||||
|
errorMessage:
|
||||||
|
widget.feedController.lastError == null
|
||||||
|
? null
|
||||||
|
: l10n.catalogError,
|
||||||
|
onRetry: widget.feedController.loadMore,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class _RecommendPageState extends State<RecommendPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final categories = context.watch<CategoryController>();
|
final categories = context.read<CategoryController>();
|
||||||
return ColoredBox(
|
return ColoredBox(
|
||||||
key: const Key('recommend-page'),
|
key: const Key('recommend-page'),
|
||||||
color: AppColors.appBackground,
|
color: AppColors.appBackground,
|
||||||
@@ -59,7 +59,10 @@ class _RecommendPageState extends State<RecommendPage> {
|
|||||||
child: IndexedStack(
|
child: IndexedStack(
|
||||||
index: _tab.index,
|
index: _tab.index,
|
||||||
children: [
|
children: [
|
||||||
_recommendFeed(context),
|
ListenableBuilder(
|
||||||
|
listenable: widget.feedController,
|
||||||
|
builder: (context, _) => _recommendFeed(context),
|
||||||
|
),
|
||||||
RecommendSectionsView(
|
RecommendSectionsView(
|
||||||
feedController: widget.feedController,
|
feedController: widget.feedController,
|
||||||
categories: categories,
|
categories: categories,
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final dramas = _filtered();
|
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final itemWidth =
|
final itemWidth =
|
||||||
@@ -66,55 +65,69 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
|
|||||||
key: const Key('all-dramas-scroll'),
|
key: const Key('all-dramas-scroll'),
|
||||||
physics: AppPullToRefresh.physics,
|
physics: AppPullToRefresh.physics,
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverToBoxAdapter(child: _filters(l10n)),
|
|
||||||
if (dramas.isEmpty)
|
|
||||||
SliverFillRemaining(
|
|
||||||
hasScrollBody: false,
|
|
||||||
child: EmptyState(
|
|
||||||
key: const Key('all-dramas-empty'),
|
|
||||||
icon: Icons.movie_filter_outlined,
|
|
||||||
title: l10n.homeGridFilterEmpty,
|
|
||||||
actionLabel: l10n.homeGridClearFilter,
|
|
||||||
onAction: () => setState(() {
|
|
||||||
_region = 'all';
|
|
||||||
_audio = 'all';
|
|
||||||
_genre = null;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
SliverPadding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: AppDimens.feedPageHorizontal,
|
|
||||||
),
|
|
||||||
sliver: SliverGrid(
|
|
||||||
key: const Key('all-dramas-grid'),
|
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: 3,
|
|
||||||
crossAxisSpacing: AppDimens.homeGridGap,
|
|
||||||
mainAxisSpacing: AppDimens.homeGridRowGap,
|
|
||||||
mainAxisExtent: itemExtent,
|
|
||||||
),
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(context, index) => HomeFeedCard(
|
|
||||||
drama: dramas[index],
|
|
||||||
onTap: () => widget.onOpenDrama(dramas[index]),
|
|
||||||
),
|
|
||||||
childCount: dramas.length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: ListenableBuilder(
|
||||||
padding: const EdgeInsets.only(bottom: AppDimens.spaceXl),
|
listenable: widget.feedController,
|
||||||
child: PagedListFooter(
|
builder: (context, _) => _filters(l10n),
|
||||||
loading: widget.feedController.loadingMore,
|
),
|
||||||
hasMore: widget.feedController.hasMore,
|
),
|
||||||
hasItems: dramas.isNotEmpty,
|
ListenableBuilder(
|
||||||
errorMessage: widget.feedController.lastError == null
|
listenable: widget.feedController,
|
||||||
? null
|
builder: (context, _) {
|
||||||
: l10n.catalogError,
|
final dramas = _filtered();
|
||||||
onRetry: widget.feedController.loadMore,
|
if (dramas.isEmpty) {
|
||||||
|
return SliverFillRemaining(
|
||||||
|
hasScrollBody: false,
|
||||||
|
child: EmptyState(
|
||||||
|
key: const Key('all-dramas-empty'),
|
||||||
|
icon: Icons.movie_filter_outlined,
|
||||||
|
title: l10n.homeGridFilterEmpty,
|
||||||
|
actionLabel: l10n.homeGridClearFilter,
|
||||||
|
onAction: () => setState(() {
|
||||||
|
_region = 'all';
|
||||||
|
_audio = 'all';
|
||||||
|
_genre = null;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: AppDimens.feedPageHorizontal,
|
||||||
|
),
|
||||||
|
sliver: SliverGrid(
|
||||||
|
key: const Key('all-dramas-grid'),
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 3,
|
||||||
|
crossAxisSpacing: AppDimens.homeGridGap,
|
||||||
|
mainAxisSpacing: AppDimens.homeGridRowGap,
|
||||||
|
mainAxisExtent: itemExtent,
|
||||||
|
),
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) => HomeFeedCard(
|
||||||
|
drama: dramas[index],
|
||||||
|
onTap: () => widget.onOpenDrama(dramas[index]),
|
||||||
|
),
|
||||||
|
childCount: dramas.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListenableBuilder(
|
||||||
|
listenable: widget.feedController,
|
||||||
|
builder: (context, _) => SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: AppDimens.spaceXl),
|
||||||
|
child: PagedListFooter(
|
||||||
|
loading: widget.feedController.loadingMore,
|
||||||
|
hasMore: widget.feedController.hasMore,
|
||||||
|
hasItems: _filtered().isNotEmpty,
|
||||||
|
errorMessage: widget.feedController.lastError == null
|
||||||
|
? null
|
||||||
|
: l10n.catalogError,
|
||||||
|
onRetry: widget.feedController.loadMore,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -33,60 +33,68 @@ class RecommendSectionsView extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
return ListenableBuilder(
|
||||||
final source = feedController.dramas;
|
listenable: Listenable.merge([feedController, categories]),
|
||||||
final hits = [...source]
|
builder: (context, _) {
|
||||||
..sort((a, b) => b.viewCount.compareTo(a.viewCount));
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final newest = [...source]
|
final source = feedController.dramas;
|
||||||
..sort((a, b) {
|
final hits = [...source]
|
||||||
final left = publishedAtForDrama(a);
|
..sort((a, b) => b.viewCount.compareTo(a.viewCount));
|
||||||
final right = publishedAtForDrama(b);
|
final newest = [...source]
|
||||||
if (left == null && right == null) return b.id.compareTo(a.id);
|
..sort((a, b) {
|
||||||
if (left == null) return 1;
|
final left = publishedAtForDrama(a);
|
||||||
if (right == null) return -1;
|
final right = publishedAtForDrama(b);
|
||||||
return right.compareTo(left);
|
if (left == null && right == null) return b.id.compareTo(a.id);
|
||||||
});
|
if (left == null) return 1;
|
||||||
return AppPullToRefresh(
|
if (right == null) return -1;
|
||||||
key: const Key('recommend-sections-refresh'),
|
return right.compareTo(left);
|
||||||
onRefresh: () async {
|
});
|
||||||
await Future.wait([feedController.refreshCurrent(), categories.load()]);
|
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(10).toList(),
|
||||||
|
onMore: () => onMore(RecommendSectionKind.hits),
|
||||||
|
),
|
||||||
|
const SizedBox(height: AppDimens.spaceXl),
|
||||||
|
_section(
|
||||||
|
context,
|
||||||
|
l10n,
|
||||||
|
key: 'new',
|
||||||
|
title: l10n.weeklyNewTitle,
|
||||||
|
subtitle: l10n.weeklyNewSubtitle,
|
||||||
|
dramas: newest.take(10).toList(),
|
||||||
|
onMore: () => onMore(RecommendSectionKind.newReleases),
|
||||||
|
),
|
||||||
|
const SizedBox(height: AppDimens.spaceXl),
|
||||||
|
_section(
|
||||||
|
context,
|
||||||
|
l10n,
|
||||||
|
key: 'costume',
|
||||||
|
title: l10n.costumeAwardsTitle,
|
||||||
|
subtitle: l10n.costumeAwardsSubtitle,
|
||||||
|
dramas: _costumeDramas(source),
|
||||||
|
onMore: () => onMore(RecommendSectionKind.costume),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
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(10).toList(),
|
|
||||||
onMore: () => onMore(RecommendSectionKind.hits),
|
|
||||||
),
|
|
||||||
const SizedBox(height: AppDimens.spaceXl),
|
|
||||||
_section(
|
|
||||||
context,
|
|
||||||
l10n,
|
|
||||||
key: 'new',
|
|
||||||
title: l10n.weeklyNewTitle,
|
|
||||||
subtitle: l10n.weeklyNewSubtitle,
|
|
||||||
dramas: newest.take(10).toList(),
|
|
||||||
onMore: () => onMore(RecommendSectionKind.newReleases),
|
|
||||||
),
|
|
||||||
const SizedBox(height: AppDimens.spaceXl),
|
|
||||||
_section(
|
|
||||||
context,
|
|
||||||
l10n,
|
|
||||||
key: 'costume',
|
|
||||||
title: l10n.costumeAwardsTitle,
|
|
||||||
subtitle: l10n.costumeAwardsSubtitle,
|
|
||||||
dramas: _costumeDramas(source),
|
|
||||||
onMore: () => onMore(RecommendSectionKind.costume),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ void main() {
|
|||||||
testWidgets('should_freeze_pull_offset_until_refresh_completes', (
|
testWidgets('should_freeze_pull_offset_until_refresh_completes', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
expect(AppPullToRefresh.physics.parent, isA<ClampingScrollPhysics>());
|
expect(AppPullToRefresh.physics.parent, isA<BouncingScrollPhysics>());
|
||||||
final refresh = Completer<void>();
|
final refresh = Completer<void>();
|
||||||
var calls = 0;
|
var calls = 0;
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@@ -48,7 +48,7 @@ void main() {
|
|||||||
expect(_contentOffset(tester), greaterThan(0));
|
expect(_contentOffset(tester), greaterThan(0));
|
||||||
expect(
|
expect(
|
||||||
tester.widget<Text>(find.text('Pull to refresh')).style?.color,
|
tester.widget<Text>(find.text('Pull to refresh')).style?.color,
|
||||||
AppColors.blue,
|
AppColors.navSelected,
|
||||||
);
|
);
|
||||||
|
|
||||||
await gesture.moveBy(const Offset(0, 120));
|
await gesture.moveBy(const Offset(0, 120));
|
||||||
@@ -56,7 +56,7 @@ void main() {
|
|||||||
expect(find.text('Release to refresh'), findsOneWidget);
|
expect(find.text('Release to refresh'), findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
tester.widget<Text>(find.text('Release to refresh')).style?.color,
|
tester.widget<Text>(find.text('Release to refresh')).style?.color,
|
||||||
AppColors.blue,
|
AppColors.navSelected,
|
||||||
);
|
);
|
||||||
|
|
||||||
await gesture.up();
|
await gesture.up();
|
||||||
@@ -64,7 +64,7 @@ void main() {
|
|||||||
expect(find.text('Refreshing…'), findsOneWidget);
|
expect(find.text('Refreshing…'), findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
tester.widget<Text>(find.text('Refreshing…')).style?.color,
|
tester.widget<Text>(find.text('Refreshing…')).style?.color,
|
||||||
AppColors.blue,
|
AppColors.navSelected,
|
||||||
);
|
);
|
||||||
expect(calls, 1);
|
expect(calls, 1);
|
||||||
final releaseOffset = _contentOffset(tester);
|
final releaseOffset = _contentOffset(tester);
|
||||||
|
|||||||
Reference in new issue
Block a user