feat(notifications): category list with tap-to-filter navigation
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:control_panel/src/features/notifications/presentation/widgets/no
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:legacy_ui/legacy_ui.dart';
|
||||
import 'package:navigation/navigation.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
@@ -19,13 +20,14 @@ class NotificationsScreen extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final primaryColor = context.sfColors.legacyPrimary;
|
||||
final selectedType = ref.watch(notificationsFilterProvider);
|
||||
final feedAsync = ref.watch(notificationsFeedProvider);
|
||||
|
||||
ref.listen(notificationsFeedProvider, (prev, next) async {
|
||||
if (next.hasError && prev?.hasError != true) {
|
||||
await showErrorDialog(context, I18n.alertsLoadError);
|
||||
}
|
||||
});
|
||||
if (selectedType != null) {
|
||||
return _FilteredNotificationsScreen(
|
||||
type: selectedType,
|
||||
onBack: () =>
|
||||
ref.read(notificationsFilterProvider.notifier).select(null),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
@@ -53,81 +55,176 @@ class NotificationsScreen extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
_FilterChips(
|
||||
selectedType: selectedType,
|
||||
onSelected: ref.read(notificationsFilterProvider.notifier).select,
|
||||
),
|
||||
Expanded(
|
||||
child: feedAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, __) => Center(
|
||||
child: Text(context.translate(I18n.alertsLoadError)),
|
||||
body: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 14, big: 12),
|
||||
vertical: SizeUtils.getByScreen(small: 12, big: 10),
|
||||
),
|
||||
child: Column(
|
||||
children: _notificationTypes.map((entry) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: SizeUtils.getByScreen(small: 8, big: 7),
|
||||
),
|
||||
data: (feed) => _NotificationsList(
|
||||
notifications: feed.notifications,
|
||||
isLoadingMore: feed.isLoadingMore,
|
||||
child: SectionButton(
|
||||
onPressed: () => ref
|
||||
.read(notificationsFilterProvider.notifier)
|
||||
.select(entry.type),
|
||||
icon: Icon(
|
||||
entry.icon,
|
||||
size: SizeUtils.getByScreen(small: 34, big: 38),
|
||||
color: primaryColor,
|
||||
),
|
||||
iconPadding: SizeUtils.getByScreen(small: 14, big: 12),
|
||||
body: Text(
|
||||
context.translate(entry.labelKey),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 16, big: 17),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FilterChips extends StatelessWidget {
|
||||
final String? selectedType;
|
||||
final ValueChanged<String?> onSelected;
|
||||
class _NotificationType {
|
||||
final String type;
|
||||
final String labelKey;
|
||||
final IconData icon;
|
||||
|
||||
const _FilterChips({required this.selectedType, required this.onSelected});
|
||||
const _NotificationType({
|
||||
required this.type,
|
||||
required this.labelKey,
|
||||
required this.icon,
|
||||
});
|
||||
}
|
||||
|
||||
const _notificationTypes = [
|
||||
_NotificationType(
|
||||
type: 'sos',
|
||||
labelKey: I18n.alertTypeSos,
|
||||
icon: Icons.sos,
|
||||
),
|
||||
_NotificationType(
|
||||
type: 'falldown',
|
||||
labelKey: I18n.alertTypeFalldown,
|
||||
icon: Icons.personal_injury,
|
||||
),
|
||||
_NotificationType(
|
||||
type: 'lowBattery',
|
||||
labelKey: I18n.alertTypeLowBattery,
|
||||
icon: Icons.battery_alert,
|
||||
),
|
||||
_NotificationType(
|
||||
type: 'disconnect',
|
||||
labelKey: I18n.alertTypeDisconnect,
|
||||
icon: Icons.link_off,
|
||||
),
|
||||
_NotificationType(
|
||||
type: 'geofenceIn',
|
||||
labelKey: I18n.alertTypeGeofenceIn,
|
||||
icon: Icons.location_on,
|
||||
),
|
||||
_NotificationType(
|
||||
type: 'geofenceOut',
|
||||
labelKey: I18n.alertTypeGeofenceOut,
|
||||
icon: Icons.location_off,
|
||||
),
|
||||
_NotificationType(
|
||||
type: 'braceletRemoved',
|
||||
labelKey: I18n.alertTypeBraceletRemoved,
|
||||
icon: Icons.watch_off,
|
||||
),
|
||||
_NotificationType(
|
||||
type: 'abnormalHeartRate',
|
||||
labelKey: I18n.alertTypeAbnormalHeartRate,
|
||||
icon: Icons.heart_broken,
|
||||
),
|
||||
_NotificationType(
|
||||
type: 'standstill',
|
||||
labelKey: I18n.alertTypeStandstill,
|
||||
icon: Icons.accessibility_new,
|
||||
),
|
||||
_NotificationType(
|
||||
type: 'movement',
|
||||
labelKey: I18n.alertTypeMovement,
|
||||
icon: Icons.directions_walk,
|
||||
),
|
||||
];
|
||||
|
||||
class _FilteredNotificationsScreen extends ConsumerWidget {
|
||||
final String type;
|
||||
final VoidCallback onBack;
|
||||
|
||||
const _FilteredNotificationsScreen({
|
||||
required this.type,
|
||||
required this.onBack,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final primaryColor = context.sfColors.legacyPrimary;
|
||||
final feedAsync = ref.watch(notificationsFeedProvider);
|
||||
|
||||
final filters = [
|
||||
(null, context.translate(I18n.alertsFilterAll)),
|
||||
('sos', 'SOS'),
|
||||
('falldown', context.translate(I18n.alertTypeFalldown)),
|
||||
('lowBattery', context.translate(I18n.alertTypeLowBattery)),
|
||||
('disconnect', context.translate(I18n.alertTypeDisconnect)),
|
||||
('geofenceIn', context.translate(I18n.alertTypeGeofenceIn)),
|
||||
('geofenceOut', context.translate(I18n.alertTypeGeofenceOut)),
|
||||
];
|
||||
final typeEntry = _notificationTypes
|
||||
.where((e) => e.type == type)
|
||||
.firstOrNull;
|
||||
final title = typeEntry != null
|
||||
? context.translate(typeEntry.labelKey)
|
||||
: type;
|
||||
|
||||
return SizedBox(
|
||||
height: SizeUtils.getByScreen<double>(small: 44, big: 40),
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
vertical: SizeUtils.getByScreen(small: 4, big: 3),
|
||||
),
|
||||
itemCount: filters.length,
|
||||
separatorBuilder: (_, __) => SizedBox(
|
||||
width: SizeUtils.getByScreen(small: 6, big: 5),
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final (type, label) = filters[index];
|
||||
final isSelected = type == selectedType;
|
||||
ref.listen(notificationsFeedProvider, (prev, next) async {
|
||||
if (next.hasError && prev?.hasError != true) {
|
||||
await showErrorDialog(context, I18n.alertsLoadError);
|
||||
}
|
||||
});
|
||||
|
||||
return ChoiceChip(
|
||||
label: Text(label),
|
||||
selected: isSelected,
|
||||
selectedColor: primaryColor,
|
||||
labelStyle: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 12, big: 11),
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isSelected ? Colors.white : primaryColor,
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (!didPop) onBack();
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
leading: IconButton(
|
||||
onPressed: onBack,
|
||||
icon: Icon(
|
||||
Icons.adaptive.arrow_back,
|
||||
color: primaryColor,
|
||||
size: SizeUtils.getByScreen(small: 32, big: 28),
|
||||
),
|
||||
side: BorderSide(color: primaryColor),
|
||||
onSelected: (_) => onSelected(type),
|
||||
visualDensity: VisualDensity.compact,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
title.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 20, big: 19),
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: feedAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, __) => Center(
|
||||
child: Text(context.translate(I18n.alertsLoadError)),
|
||||
),
|
||||
data: (feed) => _NotificationsList(
|
||||
notifications: feed.notifications,
|
||||
isLoadingMore: feed.isLoadingMore,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user