📱 Responsive for desktop
This commit is contained in:
236
lib/widgets/content/paging_helper_ext.dart
Normal file
236
lib/widgets/content/paging_helper_ext.dart
Normal file
@ -0,0 +1,236 @@
|
||||
// ignore_for_file: implementation_imports, invalid_use_of_internal_member
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:riverpod_paging_utils/src/paging_data.dart';
|
||||
import 'package:riverpod_paging_utils/src/paging_helper_view_theme.dart';
|
||||
import 'package:riverpod_paging_utils/src/paging_notifier_mixin.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
/// A generic widget for pagination.
|
||||
///
|
||||
/// Main features:
|
||||
/// 1. Displays the widget created by [contentBuilder] when data is available.
|
||||
/// 2. Shows a CircularProgressIndicator while loading the first page.
|
||||
/// 3. Displays an error widget when there is an error on the first page.
|
||||
/// 4. Shows error messages using a SnackBar.
|
||||
/// 5. Loads the next page when the last item is displayed.
|
||||
/// 6. Supports pull-to-refresh functionality.
|
||||
///
|
||||
/// You can customize the appearance of the loading view, error view, and endItemView using [PagingHelperViewTheme].
|
||||
final class PagingHelperSliverView<D extends PagingData<I>, I>
|
||||
extends ConsumerWidget {
|
||||
const PagingHelperSliverView({
|
||||
required this.provider,
|
||||
required this.futureRefreshable,
|
||||
required this.notifierRefreshable,
|
||||
required this.contentBuilder,
|
||||
this.showSecondPageError = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ProviderListenable<AsyncValue<D>> provider;
|
||||
final Refreshable<Future<D>> futureRefreshable;
|
||||
final Refreshable<PagingNotifierMixin<D, I>> notifierRefreshable;
|
||||
|
||||
/// Specifies a function that returns a widget to display when data is available.
|
||||
/// endItemView is a widget to detect when the last displayed item is visible.
|
||||
/// If endItemView is non-null, it is displayed at the end of the list.
|
||||
final Widget Function(D data, int widgetCount, Widget endItemView)
|
||||
contentBuilder;
|
||||
|
||||
final bool showSecondPageError;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context).extension<PagingHelperViewTheme>();
|
||||
|
||||
final loadingBuilder =
|
||||
theme?.loadingViewBuilder ??
|
||||
(context) => SliverFillRemaining(
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
final errorBuilder =
|
||||
theme?.errorViewBuilder ??
|
||||
(context, e, st, onPressed) => SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: onPressed,
|
||||
icon: const Icon(Icons.refresh),
|
||||
),
|
||||
Text(e.toString()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return ref
|
||||
.watch(provider)
|
||||
.whenIgnorableError(
|
||||
data: (
|
||||
data, {
|
||||
required hasError,
|
||||
required isLoading,
|
||||
required error,
|
||||
}) {
|
||||
final content = contentBuilder(
|
||||
data,
|
||||
// Add 1 to the length to include the endItemView
|
||||
data.items.length + 1,
|
||||
switch ((data.hasMore, hasError, isLoading)) {
|
||||
// Display a widget to detect when the last element is reached
|
||||
// if there are more pages and no errors
|
||||
(true, false, _) => _EndVDLoadingItemView(
|
||||
onScrollEnd:
|
||||
() async => ref.read(notifierRefreshable).loadNext(),
|
||||
),
|
||||
(true, true, false) when showSecondPageError =>
|
||||
_EndErrorItemView(
|
||||
error: error,
|
||||
onRetryButtonPressed:
|
||||
() async => ref.read(notifierRefreshable).loadNext(),
|
||||
),
|
||||
(true, true, true) => const _EndLoadingItemView(),
|
||||
_ => const SizedBox.shrink(),
|
||||
},
|
||||
);
|
||||
|
||||
return content;
|
||||
},
|
||||
// Loading state for the first page
|
||||
loading: () => loadingBuilder(context),
|
||||
// Error state for the first page
|
||||
error:
|
||||
(e, st) => errorBuilder(
|
||||
context,
|
||||
e,
|
||||
st,
|
||||
() => ref.read(notifierRefreshable).forceRefresh(),
|
||||
),
|
||||
// Prioritize data for errors on the second page and beyond
|
||||
skipErrorOnHasValue: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class _EndLoadingItemView extends StatelessWidget {
|
||||
const _EndLoadingItemView();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context).extension<PagingHelperViewTheme>();
|
||||
final childBuilder =
|
||||
theme?.endLoadingViewBuilder ??
|
||||
(context) => const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
|
||||
return childBuilder(context);
|
||||
}
|
||||
}
|
||||
|
||||
final class _EndVDLoadingItemView extends StatelessWidget {
|
||||
const _EndVDLoadingItemView({required this.onScrollEnd});
|
||||
final VoidCallback onScrollEnd;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return VisibilityDetector(
|
||||
key: key ?? const Key('EndItem'),
|
||||
onVisibilityChanged: (info) {
|
||||
if (info.visibleFraction > 0.1) {
|
||||
onScrollEnd();
|
||||
}
|
||||
},
|
||||
child: const _EndLoadingItemView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class _EndErrorItemView extends StatelessWidget {
|
||||
const _EndErrorItemView({
|
||||
required this.error,
|
||||
required this.onRetryButtonPressed,
|
||||
});
|
||||
final Object? error;
|
||||
final VoidCallback onRetryButtonPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context).extension<PagingHelperViewTheme>();
|
||||
final childBuilder =
|
||||
theme?.endErrorViewBuilder ??
|
||||
(context, e, onPressed) => Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: onPressed,
|
||||
icon: const Icon(Icons.refresh),
|
||||
),
|
||||
Text(error.toString()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return childBuilder(context, error, onRetryButtonPressed);
|
||||
}
|
||||
}
|
||||
|
||||
extension _AsyncValueX<T> on AsyncValue<T> {
|
||||
/// Extends the [when] method to handle async data states more effectively,
|
||||
/// especially when maintaining data integrity despite errors.
|
||||
///
|
||||
/// Use `skipErrorOnHasValue` to retain and display existing data
|
||||
/// even if subsequent fetch attempts result in errors,
|
||||
/// ideal for maintaining a seamless user experience.
|
||||
R whenIgnorableError<R>({
|
||||
required R Function(
|
||||
T data, {
|
||||
required bool hasError,
|
||||
required bool isLoading,
|
||||
required Object? error,
|
||||
})
|
||||
data,
|
||||
required R Function(Object error, StackTrace stackTrace) error,
|
||||
required R Function() loading,
|
||||
bool skipLoadingOnReload = false,
|
||||
bool skipLoadingOnRefresh = true,
|
||||
bool skipError = false,
|
||||
bool skipErrorOnHasValue = false,
|
||||
}) {
|
||||
if (skipErrorOnHasValue) {
|
||||
if (hasValue && hasError) {
|
||||
return data(
|
||||
requireValue,
|
||||
hasError: true,
|
||||
isLoading: isLoading,
|
||||
error: this.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return when(
|
||||
skipLoadingOnReload: skipLoadingOnReload,
|
||||
skipLoadingOnRefresh: skipLoadingOnRefresh,
|
||||
skipError: skipError,
|
||||
data:
|
||||
(d) => data(
|
||||
d,
|
||||
hasError: hasError,
|
||||
isLoading: isLoading,
|
||||
error: this.error,
|
||||
),
|
||||
error: error,
|
||||
loading: loading,
|
||||
);
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:super_context_menu/super_context_menu.dart';
|
||||
|
||||
class PostItem extends HookConsumerWidget {
|
||||
final Color? backgroundColor;
|
||||
final SnPost item;
|
||||
final EdgeInsets? padding;
|
||||
final bool isOpenable;
|
||||
@ -25,6 +26,7 @@ class PostItem extends HookConsumerWidget {
|
||||
const PostItem({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.backgroundColor,
|
||||
this.padding,
|
||||
this.isOpenable = true,
|
||||
this.onRefresh,
|
||||
@ -96,6 +98,7 @@ class PostItem extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
child: Material(
|
||||
color: backgroundColor,
|
||||
child: Padding(
|
||||
padding: renderingPadding,
|
||||
child: Column(
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/content/paging_helper_ext.dart';
|
||||
import 'package:island/widgets/post/post_item.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||
@ -53,7 +54,7 @@ class SliverPostList extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return PagingHelperView(
|
||||
return PagingHelperSliverView(
|
||||
provider: postListNotifierProvider,
|
||||
futureRefreshable: postListNotifierProvider.future,
|
||||
notifierRefreshable: postListNotifierProvider.notifier,
|
||||
|
@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/screens/account/me/publishers.dart';
|
||||
import 'package:island/screens/creators/publishers.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/post/publishers_modal.dart';
|
||||
|
@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/post/post_item.dart';
|
||||
import 'package:island/widgets/response.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
@ -14,6 +16,7 @@ class PostRepliesList extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final postAsync = ref.watch(postRepliesProvider(postId));
|
||||
final isWide = isWideScreen(context);
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh:
|
||||
@ -37,7 +40,10 @@ class PostRepliesList extends HookConsumerWidget {
|
||||
onFetchData: controller.fetchMore,
|
||||
itemBuilder: (context, index) {
|
||||
final post = controller.posts[index];
|
||||
return PostItem(item: post);
|
||||
return PostItem(
|
||||
item: post,
|
||||
backgroundColor: isWide ? Colors.transparent : null,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
emptyBuilder: (context) {
|
||||
@ -55,11 +61,9 @@ class PostRepliesList extends HookConsumerWidget {
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(e, _) => GestureDetector(
|
||||
child: Center(
|
||||
child: Text('Error: $e', textAlign: TextAlign.center),
|
||||
),
|
||||
onTap: () {
|
||||
(e, _) => ResponseErrorWidget(
|
||||
error: e,
|
||||
onRetry: () {
|
||||
ref.invalidate(postRepliesProvider(postId));
|
||||
},
|
||||
),
|
||||
|
@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/route.gr.dart';
|
||||
import 'package:island/screens/account/me/publishers.dart';
|
||||
import 'package:island/screens/creators/publishers.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
|
@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class ResponseErrorWidget extends StatelessWidget {
|
||||
final dynamic error;
|
||||
@ -19,11 +20,14 @@ class ResponseErrorWidget extends StatelessWidget {
|
||||
children: [
|
||||
const Icon(Symbols.error_outline, size: 48),
|
||||
const Gap(4),
|
||||
Text(
|
||||
error.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Color(0xFF757575)),
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 320),
|
||||
child: Text(
|
||||
error.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Color(0xFF757575)),
|
||||
),
|
||||
).center(),
|
||||
const Gap(8),
|
||||
TextButton(onPressed: onRetry, child: const Text('retry').tr()),
|
||||
],
|
||||
|
Reference in New Issue
Block a user