Better loading animation in paginationed list

This commit is contained in:
2025-12-06 13:13:30 +08:00
parent 9d03faf594
commit 38a15bb62a
4 changed files with 197 additions and 148 deletions

View File

@@ -3,6 +3,7 @@ import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/account.dart';
@@ -10,7 +11,6 @@ import 'package:island/pods/network.dart';
import 'package:island/pods/paging.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/route.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:island/widgets/content/sheet.dart';
@@ -101,8 +101,7 @@ class NotificationListNotifier extends AsyncNotifier<List<SnNotification>>
queryParameters: queryParams,
);
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final notifications =
response.data
final notifications = response.data
.map((json) => SnNotification.fromJson(json))
.cast<SnNotification>()
.toList();
@@ -145,15 +144,22 @@ class NotificationSheet extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Refresh unread count when sheet opens to sync across devices
useEffect(() {
Future(() {
ref.read(notificationUnreadCountProvider.notifier).refresh();
});
return null;
}, []);
final isLoading = useState(false);
Future<void> markAllRead() async {
showLoadingModal(context);
isLoading.value = true;
final apiClient = ref.watch(apiClientProvider);
await apiClient.post('/ring/notifications/all/read');
if (!context.mounted) return;
hideLoadingModal(context);
ref.invalidate(notificationListProvider);
isLoading.value = false;
ref.read(notificationListProvider.notifier).refresh();
ref.watch(notificationUnreadCountProvider.notifier).clear();
}
@@ -165,6 +171,14 @@ class NotificationSheet extends HookConsumerWidget {
icon: const Icon(Symbols.mark_as_unread),
),
],
child: Column(
children: [
if (isLoading.value)
LinearProgressIndicator(
minHeight: 2,
color: Theme.of(context).colorScheme.primary,
),
Expanded(
child: PaginationList(
provider: notificationListProvider,
notifier: notificationListProvider.notifier,
@@ -175,16 +189,21 @@ class NotificationSheet extends HookConsumerWidget {
return ListTile(
isThreeLine: true,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading:
pfp != null
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
leading: pfp != null
? ProfilePictureWidget(fileId: pfp, radius: 20)
: CircleAvatar(
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
backgroundColor: Theme.of(
context,
).colorScheme.primaryContainer,
child: Icon(
_getNotificationIcon(notification.topic),
color: Theme.of(context).colorScheme.onPrimaryContainer,
color: Theme.of(
context,
).colorScheme.onPrimaryContainer,
),
),
title: Text(notification.title),
@@ -197,7 +216,9 @@ class NotificationSheet extends HookConsumerWidget {
spacing: 6,
children: [
Text(
DateFormat().format(notification.createdAt.toLocal()),
DateFormat().format(
notification.createdAt.toLocal(),
),
).fontSize(11),
Text('·').fontSize(11).bold(),
Text(
@@ -209,7 +230,8 @@ class NotificationSheet extends HookConsumerWidget {
).opacity(0.75).padding(bottom: 4),
MarkdownTextContent(
content: notification.content,
textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
textStyle: Theme.of(context).textTheme.bodyMedium
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.8),
@@ -221,8 +243,7 @@ class NotificationSheet extends HookConsumerWidget {
child: Wrap(
spacing: 8,
runSpacing: 8,
children:
imageIds.map((imageId) {
children: imageIds.map((imageId) {
return SizedBox(
width: 80,
height: 80,
@@ -240,8 +261,7 @@ class NotificationSheet extends HookConsumerWidget {
),
],
),
trailing:
notification.viewedAt != null
trailing: notification.viewedAt != null
? null
: Container(
width: 12,
@@ -268,6 +288,9 @@ class NotificationSheet extends HookConsumerWidget {
);
},
),
),
],
),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/misc.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/paging.dart';
@@ -7,6 +8,7 @@ import 'package:island/widgets/extended_refresh_indicator.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
import 'package:visibility_detector/visibility_detector.dart';
@@ -48,8 +50,7 @@ class PaginationList<T> extends HookConsumerWidget {
return isSliver ? SliverFillRemaining(child: content) : content;
}
final listView =
isSliver
final listView = isSliver
? SuperSliverList.builder(
itemCount: (data.value?.length ?? 0) + 1,
itemBuilder: (context, idx) {
@@ -124,26 +125,40 @@ class PaginationWidget<T> extends HookConsumerWidget {
}
}
class PaginationListFooter<T> extends StatelessWidget {
class PaginationListFooter<T> extends HookConsumerWidget {
final PaginationController<T> noti;
final AsyncValue<List<T>> data;
final Widget? skeletonChild;
final bool isSliver;
const PaginationListFooter({
super.key,
required this.noti,
required this.data,
this.skeletonChild,
this.isSliver = false,
});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final hasBeenVisible = useState(false);
final placeholder = Skeletonizer(
enabled: true,
child:
skeletonChild ??
ListTile(
title: Text('Some data'),
subtitle: const Text('Subtitle here'),
trailing: const Icon(Icons.ac_unit),
),
);
final child = SizedBox(
height: 64,
child: Center(
child:
data.isLoading
? CircularProgressIndicator()
child: hasBeenVisible.value
? data.isLoading
? placeholder
: Row(
spacing: 8,
mainAxisAlignment: MainAxisAlignment.center,
@@ -151,13 +166,15 @@ class PaginationListFooter<T> extends StatelessWidget {
const Icon(Symbols.close, size: 16),
Text('noFurtherData').tr().fontSize(13),
],
).opacity(0.9),
).opacity(0.9)
: placeholder,
).padding(all: 8),
);
return VisibilityDetector(
key: Key("pagination-list-${noti.hashCode}"),
onVisibilityChanged: (VisibilityInfo info) {
hasBeenVisible.value = true;
if (!noti.fetchedAll && !data.isLoading && !data.hasError) {
noti.fetchFurther();
}

View File

@@ -2486,6 +2486,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.1"
skeletonizer:
dependency: "direct main"
description:
name: skeletonizer
sha256: "5d2d44120916cc749ede54c236cef60c2478742806df0b1f065212f00721b185"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
sky_engine:
dependency: transitive
description: flutter

View File

@@ -170,6 +170,7 @@ dependencies:
flutter_animate: ^4.5.2
http_parser: ^4.1.2
flutter_code_editor: ^0.3.5
skeletonizer: ^2.1.1
dev_dependencies:
flutter_test: