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/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:legacy_theme/legacy_theme.dart';
|
import 'package:legacy_theme/legacy_theme.dart';
|
||||||
|
import 'package:legacy_ui/legacy_ui.dart';
|
||||||
import 'package:navigation/navigation.dart';
|
import 'package:navigation/navigation.dart';
|
||||||
import 'package:sf_localizations/sf_localizations.dart';
|
import 'package:sf_localizations/sf_localizations.dart';
|
||||||
import 'package:sf_shared/sf_shared.dart';
|
import 'package:sf_shared/sf_shared.dart';
|
||||||
@@ -19,13 +20,14 @@ class NotificationsScreen extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final primaryColor = context.sfColors.legacyPrimary;
|
final primaryColor = context.sfColors.legacyPrimary;
|
||||||
final selectedType = ref.watch(notificationsFilterProvider);
|
final selectedType = ref.watch(notificationsFilterProvider);
|
||||||
final feedAsync = ref.watch(notificationsFeedProvider);
|
|
||||||
|
|
||||||
ref.listen(notificationsFeedProvider, (prev, next) async {
|
if (selectedType != null) {
|
||||||
if (next.hasError && prev?.hasError != true) {
|
return _FilteredNotificationsScreen(
|
||||||
await showErrorDialog(context, I18n.alertsLoadError);
|
type: selectedType,
|
||||||
}
|
onBack: () =>
|
||||||
});
|
ref.read(notificationsFilterProvider.notifier).select(null),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
@@ -53,81 +55,176 @@ class NotificationsScreen extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Column(
|
body: SingleChildScrollView(
|
||||||
children: [
|
padding: EdgeInsets.symmetric(
|
||||||
_FilterChips(
|
horizontal: SizeUtils.getByScreen(small: 14, big: 12),
|
||||||
selectedType: selectedType,
|
vertical: SizeUtils.getByScreen(small: 12, big: 10),
|
||||||
onSelected: ref.read(notificationsFilterProvider.notifier).select,
|
),
|
||||||
),
|
child: Column(
|
||||||
Expanded(
|
children: _notificationTypes.map((entry) {
|
||||||
child: feedAsync.when(
|
return Padding(
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
padding: EdgeInsets.only(
|
||||||
error: (_, __) => Center(
|
bottom: SizeUtils.getByScreen(small: 8, big: 7),
|
||||||
child: Text(context.translate(I18n.alertsLoadError)),
|
|
||||||
),
|
),
|
||||||
data: (feed) => _NotificationsList(
|
child: SectionButton(
|
||||||
notifications: feed.notifications,
|
onPressed: () => ref
|
||||||
isLoadingMore: feed.isLoadingMore,
|
.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 {
|
class _NotificationType {
|
||||||
final String? selectedType;
|
final String type;
|
||||||
final ValueChanged<String?> onSelected;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final primaryColor = context.sfColors.legacyPrimary;
|
final primaryColor = context.sfColors.legacyPrimary;
|
||||||
|
final feedAsync = ref.watch(notificationsFeedProvider);
|
||||||
|
|
||||||
final filters = [
|
final typeEntry = _notificationTypes
|
||||||
(null, context.translate(I18n.alertsFilterAll)),
|
.where((e) => e.type == type)
|
||||||
('sos', 'SOS'),
|
.firstOrNull;
|
||||||
('falldown', context.translate(I18n.alertTypeFalldown)),
|
final title = typeEntry != null
|
||||||
('lowBattery', context.translate(I18n.alertTypeLowBattery)),
|
? context.translate(typeEntry.labelKey)
|
||||||
('disconnect', context.translate(I18n.alertTypeDisconnect)),
|
: type;
|
||||||
('geofenceIn', context.translate(I18n.alertTypeGeofenceIn)),
|
|
||||||
('geofenceOut', context.translate(I18n.alertTypeGeofenceOut)),
|
|
||||||
];
|
|
||||||
|
|
||||||
return SizedBox(
|
ref.listen(notificationsFeedProvider, (prev, next) async {
|
||||||
height: SizeUtils.getByScreen<double>(small: 44, big: 40),
|
if (next.hasError && prev?.hasError != true) {
|
||||||
child: ListView.separated(
|
await showErrorDialog(context, I18n.alertsLoadError);
|
||||||
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;
|
|
||||||
|
|
||||||
return ChoiceChip(
|
return PopScope(
|
||||||
label: Text(label),
|
canPop: false,
|
||||||
selected: isSelected,
|
onPopInvokedWithResult: (didPop, _) {
|
||||||
selectedColor: primaryColor,
|
if (!didPop) onBack();
|
||||||
labelStyle: TextStyle(
|
},
|
||||||
fontSize: SizeUtils.getByScreen(small: 12, big: 11),
|
child: Scaffold(
|
||||||
fontWeight: FontWeight.w600,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
color: isSelected ? Colors.white : primaryColor,
|
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),
|
title: Text(
|
||||||
visualDensity: VisualDensity.compact,
|
title.toUpperCase(),
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
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