162 lines
5.6 KiB
Dart
162 lines
5.6 KiB
Dart
// Author: fruit.com.cn
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:flutter/material.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_list_footer.dart';
|
|
import '../../l10n/app_localizations.dart';
|
|
|
|
class FollowingPagedGrid extends StatelessWidget {
|
|
const FollowingPagedGrid({
|
|
super.key,
|
|
required this.scrollController,
|
|
required this.loading,
|
|
required this.loadingMore,
|
|
required this.hasMore,
|
|
required this.hasItems,
|
|
required this.itemCount,
|
|
required this.itemBuilder,
|
|
required this.empty,
|
|
required this.onRefresh,
|
|
required this.onLoadMore,
|
|
this.showInitialLoading = true,
|
|
this.singleColumn = false,
|
|
this.errorMessage,
|
|
this.loadMoreError,
|
|
});
|
|
|
|
static const double _loadMoreThreshold = 240;
|
|
|
|
final ScrollController scrollController;
|
|
final bool loading;
|
|
final bool loadingMore;
|
|
final bool hasMore;
|
|
final bool hasItems;
|
|
final int itemCount;
|
|
final IndexedWidgetBuilder itemBuilder;
|
|
final Widget empty;
|
|
final String? errorMessage;
|
|
final String? loadMoreError;
|
|
final bool showInitialLoading;
|
|
final bool singleColumn;
|
|
final Future<void> Function() onRefresh;
|
|
final Future<void> Function() onLoadMore;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (showInitialLoading && loading && !hasItems) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final gridWidth =
|
|
constraints.maxWidth - AppDimens.feedPageHorizontal * 2;
|
|
final itemWidth = (gridWidth - AppDimens.followingGridGap * 2) / 3;
|
|
final itemExtent =
|
|
itemWidth / AppDimens.cardCoverAspectRatio +
|
|
AppDimens.followingCardTextExtent;
|
|
final gridDelegate = singleColumn
|
|
? const SliverGridDelegateWithFixedCrossAxisCount(
|
|
crossAxisCount: 1,
|
|
mainAxisSpacing: AppDimens.followingHistoryRowGap,
|
|
mainAxisExtent: AppDimens.followingHistoryCoverHeight,
|
|
)
|
|
: SliverGridDelegateWithFixedCrossAxisCount(
|
|
crossAxisCount: 3,
|
|
crossAxisSpacing: AppDimens.followingGridGap,
|
|
mainAxisSpacing: AppDimens.followingGridRowGap,
|
|
mainAxisExtent: itemExtent,
|
|
);
|
|
return NotificationListener<ScrollNotification>(
|
|
onNotification: _onScroll,
|
|
child: AppPullToRefresh(
|
|
onRefresh: onRefresh,
|
|
child: CustomScrollView(
|
|
key: ValueKey<String>('following-scroll-$runtimeType'),
|
|
controller: scrollController,
|
|
physics: AppPullToRefresh.physics,
|
|
slivers: [
|
|
if (itemCount == 0)
|
|
SliverFillRemaining(
|
|
hasScrollBody: false,
|
|
child: errorMessage == null
|
|
? empty
|
|
: EmptyState(
|
|
icon: Icons.cloud_off_rounded,
|
|
title: errorMessage!,
|
|
actionLabel: AppLocalizations.of(context)!.retry,
|
|
onAction: () => unawaited(onRefresh()),
|
|
),
|
|
)
|
|
else ...[
|
|
if (errorMessage != null)
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(
|
|
AppDimens.feedPageHorizontal,
|
|
AppDimens.spaceSm,
|
|
AppDimens.feedPageHorizontal,
|
|
0,
|
|
),
|
|
child: Text(
|
|
errorMessage!,
|
|
style: const TextStyle(
|
|
color: AppColors.textTertiary,
|
|
fontSize: AppDimens.followingSubtitleFont,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SliverPadding(
|
|
padding: const EdgeInsets.fromLTRB(
|
|
AppDimens.feedPageHorizontal,
|
|
0,
|
|
AppDimens.feedPageHorizontal,
|
|
AppDimens.spaceSm,
|
|
),
|
|
sliver: SliverGrid(
|
|
gridDelegate: gridDelegate,
|
|
delegate: SliverChildBuilderDelegate(
|
|
itemBuilder,
|
|
childCount: itemCount,
|
|
),
|
|
),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: PagedListFooter(
|
|
loading: loadingMore,
|
|
hasMore: hasMore,
|
|
hasItems: hasItems,
|
|
errorMessage: loadMoreError,
|
|
onRetry: onLoadMore,
|
|
),
|
|
),
|
|
const SliverToBoxAdapter(
|
|
child: SizedBox(height: AppDimens.spaceXl),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
bool _onScroll(ScrollNotification notification) {
|
|
if (notification.depth != 0) return false;
|
|
if (notification.metrics.extentAfter > _loadMoreThreshold) return false;
|
|
if (notification is ScrollUpdateNotification ||
|
|
notification is ScrollEndNotification ||
|
|
notification is OverscrollNotification) {
|
|
unawaited(onLoadMore());
|
|
}
|
|
return false;
|
|
}
|
|
}
|