refactor(notifications): AlertType enum + freezed feed family + subroute by type
This commit is contained in:
@@ -101,6 +101,14 @@ void configureAppRouter() {
|
||||
path: 'notifications',
|
||||
name: 'legacy_notifications',
|
||||
pageBuilder: const DeviceNotificationsBuilder().buildPage,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: ':type',
|
||||
name: 'legacy_notifications_by_type',
|
||||
pageBuilder:
|
||||
const FilteredNotificationsBuilder().buildPage,
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: 'account_settings',
|
||||
|
||||
@@ -12,6 +12,7 @@ export 'src/features/remote_on_off/remote_on_off_builder.dart';
|
||||
export 'src/features/alerts/alerts_builder.dart';
|
||||
export 'src/features/notifications/notifications_builder.dart'
|
||||
show DeviceNotificationsBuilder;
|
||||
export 'src/features/notifications/filtered/filtered_notifications_builder.dart';
|
||||
export 'src/features/sos_contacts/sos_contacts_builder.dart';
|
||||
export 'src/features/sound/sound_builder.dart';
|
||||
export 'src/features/sync_clock/sync_clock_builder.dart';
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
|
||||
enum AlertType {
|
||||
sos('sos', I18n.alertTypeSos, Icons.warning_amber_rounded),
|
||||
falldown('falldown', I18n.alertTypeFalldown, Icons.personal_injury),
|
||||
lowBattery('lowBattery', I18n.alertTypeLowBattery, Icons.battery_alert),
|
||||
disconnect('disconnect', I18n.alertTypeDisconnect, Icons.link_off),
|
||||
reconnected('reconnect', I18n.alertTypeReconnected, Icons.wifi),
|
||||
geofenceIn('geofenceIn', I18n.alertTypeGeofenceIn, Icons.location_on),
|
||||
geofenceOut('geofenceOut', I18n.alertTypeGeofenceOut, Icons.location_off),
|
||||
braceletRemoved(
|
||||
'braceletRemoved',
|
||||
I18n.alertTypeBraceletRemoved,
|
||||
Icons.watch_off,
|
||||
),
|
||||
abnormalHeartRate(
|
||||
'abnormalHeartRate',
|
||||
I18n.alertTypeAbnormalHeartRate,
|
||||
Icons.heart_broken,
|
||||
),
|
||||
standstill(
|
||||
'standstill',
|
||||
I18n.alertTypeStandstill,
|
||||
Icons.accessibility_new,
|
||||
),
|
||||
movement('movement', I18n.alertTypeMovement, Icons.directions_walk);
|
||||
|
||||
const AlertType(this.apiKey, this.labelKey, this.icon);
|
||||
|
||||
final String apiKey;
|
||||
final String labelKey;
|
||||
final IconData icon;
|
||||
|
||||
Color color(BuildContext context) => switch (this) {
|
||||
AlertType.sos => Theme.of(context).colorScheme.error,
|
||||
AlertType.falldown => Theme.of(context).colorScheme.error,
|
||||
AlertType.abnormalHeartRate => Theme.of(context).colorScheme.error,
|
||||
AlertType.lowBattery => Colors.orange,
|
||||
AlertType.disconnect => Colors.grey,
|
||||
AlertType.reconnected => Colors.green,
|
||||
AlertType.braceletRemoved => Colors.deepOrange,
|
||||
AlertType.standstill => Colors.amber,
|
||||
AlertType.geofenceIn => Colors.blue,
|
||||
AlertType.geofenceOut => Colors.indigo,
|
||||
AlertType.movement => Colors.teal,
|
||||
};
|
||||
|
||||
static AlertType? fromApiKey(String key) {
|
||||
for (final value in AlertType.values) {
|
||||
if (value.apiKey == key) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:settings/src/core/domain/entities/alert_type.dart';
|
||||
import 'package:settings/src/features/notifications/filtered/presentation/filtered_notifications_screen.dart';
|
||||
|
||||
class FilteredNotificationsBuilder {
|
||||
const FilteredNotificationsBuilder();
|
||||
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
final raw = state.pathParameters['type'] ?? '';
|
||||
final type = AlertType.fromApiKey(raw);
|
||||
|
||||
return MaterialPage<void>(
|
||||
key: state.pageKey,
|
||||
child: type == null
|
||||
? const _UnknownTypeRedirect()
|
||||
: FilteredNotificationsScreen(type: type),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UnknownTypeRedirect extends StatelessWidget {
|
||||
const _UnknownTypeRedirect();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
});
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
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:settings/src/core/domain/entities/alert_type.dart';
|
||||
import 'package:settings/src/core/domain/entities/notification_entity.dart';
|
||||
import 'package:settings/src/features/notifications/presentation/providers/notifications_feed_provider.dart';
|
||||
import 'package:settings/src/features/notifications/presentation/widgets/notification_card.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
class FilteredNotificationsScreen extends ConsumerWidget {
|
||||
final AlertType type;
|
||||
|
||||
const FilteredNotificationsScreen({super.key, required this.type});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final feedAsync = ref.watch(notificationsFeedProvider(type));
|
||||
|
||||
ref.listen(notificationsFeedProvider(type), (prev, next) async {
|
||||
if (next.hasError && prev?.hasError != true) {
|
||||
await showErrorDialog(context, I18n.alertsLoadError);
|
||||
}
|
||||
});
|
||||
|
||||
return LegacyPageLayout(
|
||||
title: context.translate(type.labelKey),
|
||||
body: feedAsync.when(
|
||||
loading: () => const LegacyLoadingIndicator(),
|
||||
error: (_, __) =>
|
||||
Center(child: Text(context.translate(I18n.alertsLoadError))),
|
||||
data: (feed) => _NotificationsList(
|
||||
type: type,
|
||||
notifications: feed.notifications,
|
||||
isLoadingMore: feed.isLoadingMore,
|
||||
hasMore: feed.hasMore,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NotificationsList extends ConsumerStatefulWidget {
|
||||
final AlertType type;
|
||||
final List<NotificationEntity> notifications;
|
||||
final bool isLoadingMore;
|
||||
final bool hasMore;
|
||||
|
||||
const _NotificationsList({
|
||||
required this.type,
|
||||
required this.notifications,
|
||||
required this.isLoadingMore,
|
||||
required this.hasMore,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<_NotificationsList> createState() => _NotificationsListState();
|
||||
}
|
||||
|
||||
class _NotificationsListState extends ConsumerState<_NotificationsList> {
|
||||
final _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_onScroll);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _ensureViewportFilled());
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _NotificationsList oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _ensureViewportFilled());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.removeListener(_onScroll);
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent - 200) {
|
||||
_triggerLoadMore();
|
||||
}
|
||||
}
|
||||
|
||||
void _ensureViewportFilled() {
|
||||
if (!mounted || !_scrollController.hasClients) return;
|
||||
if (!widget.hasMore || widget.isLoadingMore) return;
|
||||
if (_scrollController.position.maxScrollExtent <= 0) {
|
||||
_triggerLoadMore();
|
||||
}
|
||||
}
|
||||
|
||||
void _triggerLoadMore() {
|
||||
ref.read(notificationsFeedProvider(widget.type).notifier).loadMore();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final notifications = widget.notifications;
|
||||
|
||||
if (notifications.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.notifications_none,
|
||||
color: context.sfColors.legacyPrimary,
|
||||
size: SizeUtils.getByScreen(small: 80, big: 90),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 18)),
|
||||
Text(
|
||||
context.translate(I18n.alertsEmpty),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 16, big: 17),
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
vertical: SizeUtils.getByScreen(small: 8, big: 6),
|
||||
),
|
||||
itemCount: notifications.length + (widget.isLoadingMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == notifications.length) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
return NotificationCard(notification: notifications[index]);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:navigation/navigation.dart';
|
||||
|
||||
import 'presentation/notifications_screen.dart';
|
||||
import 'package:settings/src/features/notifications/presentation/notifications_screen.dart';
|
||||
|
||||
class DeviceNotificationsBuilder {
|
||||
const DeviceNotificationsBuilder();
|
||||
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
|
||||
|
||||
return MaterialPage<void>(
|
||||
key: state.pageKey,
|
||||
child: NotificationsScreen(navigationContract: navigationContract),
|
||||
child: const NotificationsScreen(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +1,48 @@
|
||||
import 'package:settings/src/core/domain/entities/notification_entity.dart';
|
||||
import 'package:settings/src/features/notifications/presentation/providers/notifications_feed_provider.dart';
|
||||
import 'package:settings/src/features/notifications/presentation/providers/notifications_filter_provider.dart';
|
||||
import 'package:settings/src/features/notifications/presentation/widgets/notification_card.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:legacy_ui/legacy_ui.dart';
|
||||
import 'package:navigation/navigation.dart';
|
||||
import 'package:settings/src/core/domain/entities/alert_type.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
class NotificationsScreen extends ConsumerWidget {
|
||||
final NavigationContract navigationContract;
|
||||
|
||||
const NotificationsScreen({super.key, required this.navigationContract});
|
||||
const NotificationsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final primaryColor = context.sfColors.legacyPrimary;
|
||||
final selectedType = ref.watch(notificationsFilterProvider);
|
||||
final device = ref.watch(selectedDeviceProvider).value;
|
||||
final supported = _supportedTypes(device?.capabilities?.alerts?.types);
|
||||
|
||||
if (selectedType != null) {
|
||||
return _FilteredNotificationsScreen(
|
||||
type: selectedType,
|
||||
onBack: () =>
|
||||
ref.read(notificationsFilterProvider.notifier).select(null),
|
||||
);
|
||||
}
|
||||
|
||||
return 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: () => navigationContract.goBack(),
|
||||
icon: Icon(
|
||||
Icons.adaptive.arrow_back,
|
||||
color: primaryColor,
|
||||
size: SizeUtils.getByScreen(small: 32, big: 28),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
context.translate(I18n.notificationsLegacyTitle).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 20, big: 19),
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
return LegacyPageLayout(
|
||||
title: context.translate(I18n.notificationsLegacyTitle),
|
||||
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) {
|
||||
children: supported.map((type) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: SizeUtils.getByScreen(small: 8, big: 7),
|
||||
),
|
||||
child: SectionButton(
|
||||
onPressed: () => ref
|
||||
.read(notificationsFilterProvider.notifier)
|
||||
.select(entry.type),
|
||||
onPressed: () => context.push(
|
||||
AppRoutes.deviceNotificationsByType(type.apiKey),
|
||||
),
|
||||
icon: Icon(
|
||||
entry.icon,
|
||||
type.icon,
|
||||
size: SizeUtils.getByScreen(small: 34, big: 38),
|
||||
color: primaryColor,
|
||||
),
|
||||
iconPadding: SizeUtils.getByScreen(small: 14, big: 12),
|
||||
body: Text(
|
||||
context.translate(entry.labelKey),
|
||||
context.translate(type.labelKey),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 16, big: 17),
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -91,225 +55,13 @@ class NotificationsScreen extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NotificationType {
|
||||
final String type;
|
||||
final String labelKey;
|
||||
final IconData icon;
|
||||
|
||||
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, WidgetRef ref) {
|
||||
final primaryColor = context.sfColors.legacyPrimary;
|
||||
final feedAsync = ref.watch(notificationsFeedProvider);
|
||||
|
||||
final typeEntry = _notificationTypes
|
||||
.where((e) => e.type == type)
|
||||
.firstOrNull;
|
||||
final title = typeEntry != null
|
||||
? context.translate(typeEntry.labelKey)
|
||||
: type;
|
||||
|
||||
ref.listen(notificationsFeedProvider, (prev, next) async {
|
||||
if (next.hasError && prev?.hasError != true) {
|
||||
await showErrorDialog(context, I18n.alertsLoadError);
|
||||
}
|
||||
});
|
||||
|
||||
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),
|
||||
),
|
||||
),
|
||||
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 LegacyLoadingIndicator(),
|
||||
error: (_, __) => Center(
|
||||
child: Text(context.translate(I18n.alertsLoadError)),
|
||||
),
|
||||
data: (feed) => _NotificationsList(
|
||||
notifications: feed.notifications,
|
||||
isLoadingMore: feed.isLoadingMore,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NotificationsList extends ConsumerStatefulWidget {
|
||||
final List<NotificationEntity> notifications;
|
||||
final bool isLoadingMore;
|
||||
|
||||
const _NotificationsList({
|
||||
required this.notifications,
|
||||
required this.isLoadingMore,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<_NotificationsList> createState() =>
|
||||
_NotificationsListState();
|
||||
}
|
||||
|
||||
class _NotificationsListState extends ConsumerState<_NotificationsList> {
|
||||
final _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_onScroll);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent - 200) {
|
||||
ref.read(notificationsFeedProvider.notifier).loadMore();
|
||||
List<AlertType> _supportedTypes(List<String>? deviceAlertTypes) {
|
||||
if (deviceAlertTypes == null || deviceAlertTypes.isEmpty) {
|
||||
return AlertType.values;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final notifications = widget.notifications;
|
||||
|
||||
if (notifications.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.notifications_none,
|
||||
color: context.sfColors.legacyPrimary,
|
||||
size: SizeUtils.getByScreen(small: 80, big: 90),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 18)),
|
||||
Text(
|
||||
context.translate(I18n.alertsEmpty),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 16, big: 17),
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
vertical: SizeUtils.getByScreen(small: 8, big: 6),
|
||||
),
|
||||
itemCount: notifications.length + (widget.isLoadingMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == notifications.length) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
return NotificationCard(notification: notifications[index]);
|
||||
},
|
||||
);
|
||||
return AlertType.values
|
||||
.where((t) => deviceAlertTypes.contains(t.apiKey))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +1,28 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:settings/src/core/domain/entities/notification_entity.dart';
|
||||
import 'package:settings/src/core/providers/notifications_repository_provider.dart';
|
||||
import 'package:settings/src/features/notifications/presentation/providers/notifications_filter_provider.dart';
|
||||
import 'package:legacy_device_state/legacy_device_state.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:settings/src/core/domain/entities/alert_type.dart';
|
||||
import 'package:settings/src/core/domain/entities/notification_entity.dart';
|
||||
import 'package:settings/src/core/providers/notifications_repository_provider.dart';
|
||||
import 'package:settings/src/features/notifications/presentation/providers/notifications_feed_state.dart';
|
||||
import 'package:sf_infrastructure/sf_infrastructure.dart';
|
||||
|
||||
part 'notifications_feed_provider.g.dart';
|
||||
|
||||
const int _pageSize = 20;
|
||||
|
||||
class NotificationsFeedState {
|
||||
const NotificationsFeedState({
|
||||
this.notifications = const [],
|
||||
this.currentPage = 1,
|
||||
this.hasMore = false,
|
||||
this.isLoadingMore = false,
|
||||
});
|
||||
|
||||
final List<NotificationEntity> notifications;
|
||||
final int currentPage;
|
||||
final bool hasMore;
|
||||
final bool isLoadingMore;
|
||||
|
||||
NotificationsFeedState copyWith({
|
||||
List<NotificationEntity>? notifications,
|
||||
int? currentPage,
|
||||
bool? hasMore,
|
||||
bool? isLoadingMore,
|
||||
}) {
|
||||
return NotificationsFeedState(
|
||||
notifications: notifications ?? this.notifications,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
hasMore: hasMore ?? this.hasMore,
|
||||
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class NotificationsFeed extends _$NotificationsFeed {
|
||||
StreamSubscription<WebSocketEvent>? _subscription;
|
||||
|
||||
@override
|
||||
Future<NotificationsFeedState> build() async {
|
||||
final filter = ref.watch(notificationsFilterProvider);
|
||||
|
||||
_subscription?.cancel();
|
||||
_subscription =
|
||||
ref.read(webSocketServiceProvider).events.listen(_onEvent);
|
||||
ref.onDispose(() => _subscription?.cancel());
|
||||
Future<NotificationsFeedState> build(AlertType type) async {
|
||||
final subscription = ref
|
||||
.read(webSocketServiceProvider)
|
||||
.events
|
||||
.listen(_onEvent);
|
||||
ref.onDispose(subscription.cancel);
|
||||
|
||||
final (notifications, totalPages) = await ref
|
||||
.read(notificationsRepositoryProvider)
|
||||
.getNotifications(page: 1, pageSize: _pageSize, type: filter);
|
||||
.getNotifications(page: 1, pageSize: _pageSize, type: type.apiKey);
|
||||
|
||||
return NotificationsFeedState(
|
||||
notifications: notifications,
|
||||
@@ -65,24 +33,23 @@ class NotificationsFeed extends _$NotificationsFeed {
|
||||
|
||||
void _onEvent(WebSocketEvent event) {
|
||||
if (event is! AlertEvent) return;
|
||||
if (event.type != type.apiKey) return;
|
||||
|
||||
final current = state.value;
|
||||
if (current == null) return;
|
||||
|
||||
final filter = ref.read(notificationsFilterProvider);
|
||||
if (filter != null && filter != event.type) return;
|
||||
|
||||
final newNotification = NotificationEntity(
|
||||
id: '',
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final injected = NotificationEntity(
|
||||
id: 'ws-${event.deviceIdentificator}-${event.type}-$now',
|
||||
deviceIdentificator: event.deviceIdentificator,
|
||||
deviceName: '',
|
||||
type: event.type,
|
||||
createdAt: DateTime.now().millisecondsSinceEpoch,
|
||||
createdAt: now,
|
||||
);
|
||||
|
||||
state = AsyncData(
|
||||
current.copyWith(
|
||||
notifications: [newNotification, ...current.notifications],
|
||||
notifications: [injected, ...current.notifications],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -95,11 +62,14 @@ class NotificationsFeed extends _$NotificationsFeed {
|
||||
state = AsyncData(current.copyWith(isLoadingMore: true));
|
||||
|
||||
try {
|
||||
final filter = ref.read(notificationsFilterProvider);
|
||||
final nextPage = current.currentPage + 1;
|
||||
final (notifications, totalPages) = await ref
|
||||
.read(notificationsRepositoryProvider)
|
||||
.getNotifications(page: nextPage, pageSize: _pageSize, type: filter);
|
||||
.getNotifications(
|
||||
page: nextPage,
|
||||
pageSize: _pageSize,
|
||||
type: type.apiKey,
|
||||
);
|
||||
|
||||
state = AsyncData(
|
||||
current.copyWith(
|
||||
@@ -110,7 +80,9 @@ class NotificationsFeed extends _$NotificationsFeed {
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
state = AsyncData(current.copyWith(isLoadingMore: false));
|
||||
state = AsyncData(
|
||||
current.copyWith(isLoadingMore: false, hasMore: false),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,38 +10,83 @@ part of 'notifications_feed_provider.dart';
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(NotificationsFeed)
|
||||
const notificationsFeedProvider = NotificationsFeedProvider._();
|
||||
const notificationsFeedProvider = NotificationsFeedFamily._();
|
||||
|
||||
final class NotificationsFeedProvider
|
||||
extends $AsyncNotifierProvider<NotificationsFeed, NotificationsFeedState> {
|
||||
const NotificationsFeedProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'notificationsFeedProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
const NotificationsFeedProvider._({
|
||||
required NotificationsFeedFamily super.from,
|
||||
required AlertType super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'notificationsFeedProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$notificationsFeedHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'notificationsFeedProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
NotificationsFeed create() => NotificationsFeed();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is NotificationsFeedProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$notificationsFeedHash() => r'907cd9e444a625ff38206809797a006321e1ed99';
|
||||
String _$notificationsFeedHash() => r'824050171b434b1951237e0b58a0e135fe8e4785';
|
||||
|
||||
final class NotificationsFeedFamily extends $Family
|
||||
with
|
||||
$ClassFamilyOverride<
|
||||
NotificationsFeed,
|
||||
AsyncValue<NotificationsFeedState>,
|
||||
NotificationsFeedState,
|
||||
FutureOr<NotificationsFeedState>,
|
||||
AlertType
|
||||
> {
|
||||
const NotificationsFeedFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'notificationsFeedProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
NotificationsFeedProvider call(AlertType type) =>
|
||||
NotificationsFeedProvider._(argument: type, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'notificationsFeedProvider';
|
||||
}
|
||||
|
||||
abstract class _$NotificationsFeed
|
||||
extends $AsyncNotifier<NotificationsFeedState> {
|
||||
FutureOr<NotificationsFeedState> build();
|
||||
late final _$args = ref.$arg as AlertType;
|
||||
AlertType get type => _$args;
|
||||
|
||||
FutureOr<NotificationsFeedState> build(AlertType type);
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final created = build(_$args);
|
||||
final ref =
|
||||
this.ref
|
||||
as $Ref<AsyncValue<NotificationsFeedState>, NotificationsFeedState>;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:settings/src/core/domain/entities/notification_entity.dart';
|
||||
|
||||
part 'notifications_feed_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class NotificationsFeedState with _$NotificationsFeedState {
|
||||
const factory NotificationsFeedState({
|
||||
@Default([]) List<NotificationEntity> notifications,
|
||||
@Default(1) int currentPage,
|
||||
@Default(false) bool hasMore,
|
||||
@Default(false) bool isLoadingMore,
|
||||
}) = _NotificationsFeedState;
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'notifications_feed_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$NotificationsFeedState {
|
||||
|
||||
List<NotificationEntity> get notifications; int get currentPage; bool get hasMore; bool get isLoadingMore;
|
||||
/// Create a copy of NotificationsFeedState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$NotificationsFeedStateCopyWith<NotificationsFeedState> get copyWith => _$NotificationsFeedStateCopyWithImpl<NotificationsFeedState>(this as NotificationsFeedState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is NotificationsFeedState&&const DeepCollectionEquality().equals(other.notifications, notifications)&&(identical(other.currentPage, currentPage) || other.currentPage == currentPage)&&(identical(other.hasMore, hasMore) || other.hasMore == hasMore)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(notifications),currentPage,hasMore,isLoadingMore);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'NotificationsFeedState(notifications: $notifications, currentPage: $currentPage, hasMore: $hasMore, isLoadingMore: $isLoadingMore)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $NotificationsFeedStateCopyWith<$Res> {
|
||||
factory $NotificationsFeedStateCopyWith(NotificationsFeedState value, $Res Function(NotificationsFeedState) _then) = _$NotificationsFeedStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
List<NotificationEntity> notifications, int currentPage, bool hasMore, bool isLoadingMore
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$NotificationsFeedStateCopyWithImpl<$Res>
|
||||
implements $NotificationsFeedStateCopyWith<$Res> {
|
||||
_$NotificationsFeedStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final NotificationsFeedState _self;
|
||||
final $Res Function(NotificationsFeedState) _then;
|
||||
|
||||
/// Create a copy of NotificationsFeedState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? notifications = null,Object? currentPage = null,Object? hasMore = null,Object? isLoadingMore = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
notifications: null == notifications ? _self.notifications : notifications // ignore: cast_nullable_to_non_nullable
|
||||
as List<NotificationEntity>,currentPage: null == currentPage ? _self.currentPage : currentPage // ignore: cast_nullable_to_non_nullable
|
||||
as int,hasMore: null == hasMore ? _self.hasMore : hasMore // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isLoadingMore: null == isLoadingMore ? _self.isLoadingMore : isLoadingMore // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [NotificationsFeedState].
|
||||
extension NotificationsFeedStatePatterns on NotificationsFeedState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _NotificationsFeedState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _NotificationsFeedState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _NotificationsFeedState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _NotificationsFeedState():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _NotificationsFeedState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _NotificationsFeedState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<NotificationEntity> notifications, int currentPage, bool hasMore, bool isLoadingMore)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _NotificationsFeedState() when $default != null:
|
||||
return $default(_that.notifications,_that.currentPage,_that.hasMore,_that.isLoadingMore);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<NotificationEntity> notifications, int currentPage, bool hasMore, bool isLoadingMore) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _NotificationsFeedState():
|
||||
return $default(_that.notifications,_that.currentPage,_that.hasMore,_that.isLoadingMore);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<NotificationEntity> notifications, int currentPage, bool hasMore, bool isLoadingMore)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _NotificationsFeedState() when $default != null:
|
||||
return $default(_that.notifications,_that.currentPage,_that.hasMore,_that.isLoadingMore);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _NotificationsFeedState implements NotificationsFeedState {
|
||||
const _NotificationsFeedState({final List<NotificationEntity> notifications = const [], this.currentPage = 1, this.hasMore = false, this.isLoadingMore = false}): _notifications = notifications;
|
||||
|
||||
|
||||
final List<NotificationEntity> _notifications;
|
||||
@override@JsonKey() List<NotificationEntity> get notifications {
|
||||
if (_notifications is EqualUnmodifiableListView) return _notifications;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_notifications);
|
||||
}
|
||||
|
||||
@override@JsonKey() final int currentPage;
|
||||
@override@JsonKey() final bool hasMore;
|
||||
@override@JsonKey() final bool isLoadingMore;
|
||||
|
||||
/// Create a copy of NotificationsFeedState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$NotificationsFeedStateCopyWith<_NotificationsFeedState> get copyWith => __$NotificationsFeedStateCopyWithImpl<_NotificationsFeedState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _NotificationsFeedState&&const DeepCollectionEquality().equals(other._notifications, _notifications)&&(identical(other.currentPage, currentPage) || other.currentPage == currentPage)&&(identical(other.hasMore, hasMore) || other.hasMore == hasMore)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_notifications),currentPage,hasMore,isLoadingMore);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'NotificationsFeedState(notifications: $notifications, currentPage: $currentPage, hasMore: $hasMore, isLoadingMore: $isLoadingMore)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$NotificationsFeedStateCopyWith<$Res> implements $NotificationsFeedStateCopyWith<$Res> {
|
||||
factory _$NotificationsFeedStateCopyWith(_NotificationsFeedState value, $Res Function(_NotificationsFeedState) _then) = __$NotificationsFeedStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
List<NotificationEntity> notifications, int currentPage, bool hasMore, bool isLoadingMore
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$NotificationsFeedStateCopyWithImpl<$Res>
|
||||
implements _$NotificationsFeedStateCopyWith<$Res> {
|
||||
__$NotificationsFeedStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _NotificationsFeedState _self;
|
||||
final $Res Function(_NotificationsFeedState) _then;
|
||||
|
||||
/// Create a copy of NotificationsFeedState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? notifications = null,Object? currentPage = null,Object? hasMore = null,Object? isLoadingMore = null,}) {
|
||||
return _then(_NotificationsFeedState(
|
||||
notifications: null == notifications ? _self._notifications : notifications // ignore: cast_nullable_to_non_nullable
|
||||
as List<NotificationEntity>,currentPage: null == currentPage ? _self.currentPage : currentPage // ignore: cast_nullable_to_non_nullable
|
||||
as int,hasMore: null == hasMore ? _self.hasMore : hasMore // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isLoadingMore: null == isLoadingMore ? _self.isLoadingMore : isLoadingMore // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -1,14 +0,0 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'notifications_filter_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class NotificationsFilter extends _$NotificationsFilter {
|
||||
@override
|
||||
String? build() => null;
|
||||
|
||||
void select(String? type) {
|
||||
if (type == state) return;
|
||||
state = type;
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'notifications_filter_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(NotificationsFilter)
|
||||
const notificationsFilterProvider = NotificationsFilterProvider._();
|
||||
|
||||
final class NotificationsFilterProvider
|
||||
extends $NotifierProvider<NotificationsFilter, String?> {
|
||||
const NotificationsFilterProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'notificationsFilterProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$notificationsFilterHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
NotificationsFilter create() => NotificationsFilter();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(String? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<String?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$notificationsFilterHash() =>
|
||||
r'3da0e65affd7f2c5121e19ab1625b5cfc14d3b3a';
|
||||
|
||||
abstract class _$NotificationsFilter extends $Notifier<String?> {
|
||||
String? build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<String?, String?>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<String?, String?>,
|
||||
String?,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:settings/src/core/domain/entities/alert_type.dart';
|
||||
import 'package:settings/src/core/domain/entities/notification_entity.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import '../../../../core/domain/entities/notification_entity.dart';
|
||||
|
||||
class NotificationCard extends ConsumerWidget {
|
||||
final NotificationEntity notification;
|
||||
|
||||
@@ -14,11 +14,15 @@ class NotificationCard extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final primaryColor = context.sfColors.legacyPrimary;
|
||||
final type = AlertType.fromApiKey(notification.type);
|
||||
final accent = type?.color(context) ?? Colors.grey;
|
||||
final label = type != null
|
||||
? context.translate(type.labelKey)
|
||||
: notification.type;
|
||||
final iconData = type?.icon ?? Icons.notifications;
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: SizeUtils.getByScreen(small: 8, big: 6),
|
||||
),
|
||||
padding: EdgeInsets.only(bottom: SizeUtils.getByScreen(small: 8, big: 6)),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 14, big: 12),
|
||||
@@ -36,13 +40,12 @@ class NotificationCard extends ConsumerWidget {
|
||||
width: SizeUtils.getByScreen(small: 40, big: 36),
|
||||
height: SizeUtils.getByScreen(small: 40, big: 36),
|
||||
decoration: BoxDecoration(
|
||||
color: _notificationColor(context, notification.type)
|
||||
.withValues(alpha: 0.15),
|
||||
color: accent.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
_notificationIcon(notification.type),
|
||||
color: _notificationColor(context, notification.type),
|
||||
iconData,
|
||||
color: accent,
|
||||
size: SizeUtils.getByScreen(small: 22, big: 20),
|
||||
),
|
||||
),
|
||||
@@ -52,26 +55,27 @@ class NotificationCard extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_notificationTypeLabel(context, notification.type),
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 14, big: 13),
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
notification.deviceName.isNotEmpty
|
||||
? notification.deviceName
|
||||
: notification.deviceIdentificator,
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 12, big: 11),
|
||||
color: Theme.of(context).colorScheme.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
if (notification.geofenceAlert != null) ...[
|
||||
SizedBox(height: 2),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${context.translate(I18n.alertGeofenceDetail)}: ${notification.geofenceAlert!.name}',
|
||||
style: TextStyle(
|
||||
@@ -87,8 +91,9 @@ class NotificationCard extends ConsumerWidget {
|
||||
_timeAgo(context, notification.createdAt),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 11, big: 10),
|
||||
color: Theme.of(context).colorScheme.onSurface
|
||||
.withValues(alpha: 0.4),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -97,66 +102,35 @@ class NotificationCard extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
IconData _notificationIcon(String type) {
|
||||
return switch (type) {
|
||||
'sos' => Icons.warning_amber_rounded,
|
||||
'falldown' => Icons.person_off,
|
||||
'lowBattery' => Icons.battery_alert,
|
||||
'disconnect' => Icons.wifi_off,
|
||||
'reconnected' => Icons.wifi,
|
||||
'braceletRemoved' => Icons.watch_off,
|
||||
'standstill' => Icons.accessibility_new,
|
||||
'abnormalHeartRate' => Icons.heart_broken,
|
||||
'geofenceIn' => Icons.login,
|
||||
'geofenceOut' => Icons.logout,
|
||||
'movement' => Icons.directions_walk,
|
||||
_ => Icons.notifications,
|
||||
};
|
||||
}
|
||||
|
||||
Color _notificationColor(BuildContext context, String type) {
|
||||
return switch (type) {
|
||||
'sos' => Theme.of(context).colorScheme.error,
|
||||
'falldown' => Theme.of(context).colorScheme.error,
|
||||
'lowBattery' => Colors.orange,
|
||||
'disconnect' => Colors.grey,
|
||||
'reconnected' => Colors.green,
|
||||
'braceletRemoved' => Colors.deepOrange,
|
||||
'standstill' => Colors.amber,
|
||||
'abnormalHeartRate' => Theme.of(context).colorScheme.error,
|
||||
'geofenceIn' => Colors.blue,
|
||||
'geofenceOut' => Colors.indigo,
|
||||
'movement' => Colors.teal,
|
||||
_ => Colors.grey,
|
||||
};
|
||||
}
|
||||
|
||||
String _notificationTypeLabel(BuildContext context, String type) {
|
||||
return switch (type) {
|
||||
'sos' => context.translate(I18n.alertTypeSos),
|
||||
'falldown' => context.translate(I18n.alertTypeFalldown),
|
||||
'lowBattery' => context.translate(I18n.alertTypeLowBattery),
|
||||
'disconnect' => context.translate(I18n.alertTypeDisconnect),
|
||||
'reconnected' => context.translate(I18n.alertTypeReconnected),
|
||||
'braceletRemoved' => context.translate(I18n.alertTypeBraceletRemoved),
|
||||
'standstill' => context.translate(I18n.alertTypeStandstill),
|
||||
'abnormalHeartRate' => context.translate(I18n.alertTypeAbnormalHeartRate),
|
||||
'geofenceIn' => context.translate(I18n.alertTypeGeofenceIn),
|
||||
'geofenceOut' => context.translate(I18n.alertTypeGeofenceOut),
|
||||
'movement' => context.translate(I18n.alertTypeMovement),
|
||||
_ => type,
|
||||
};
|
||||
}
|
||||
|
||||
String _timeAgo(BuildContext context, int timestampMs) {
|
||||
final now = DateTime.now();
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(timestampMs);
|
||||
final difference = now.difference(date);
|
||||
|
||||
if (difference.inMinutes < 1) return 'ahora';
|
||||
if (difference.inMinutes < 60) return '${difference.inMinutes} min';
|
||||
if (difference.inHours < 24) return '${difference.inHours}h';
|
||||
if (difference.inDays < 30) return '${difference.inDays}d';
|
||||
return '${(difference.inDays / 30).floor()}m';
|
||||
if (difference.inMinutes < 1) {
|
||||
return context.translate(I18n.timeAgoNow);
|
||||
}
|
||||
if (difference.inMinutes < 60) {
|
||||
return context.translate(
|
||||
I18n.timeAgoMinutes,
|
||||
args: {'count': '${difference.inMinutes}'},
|
||||
);
|
||||
}
|
||||
if (difference.inHours < 24) {
|
||||
return context.translate(
|
||||
I18n.timeAgoHours,
|
||||
args: {'count': '${difference.inHours}'},
|
||||
);
|
||||
}
|
||||
if (difference.inDays < 30) {
|
||||
return context.translate(
|
||||
I18n.timeAgoDays,
|
||||
args: {'count': '${difference.inDays}'},
|
||||
);
|
||||
}
|
||||
return context.translate(
|
||||
I18n.timeAgoMonths,
|
||||
args: {'count': '${(difference.inDays / 30).floor()}'},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:settings/src/core/domain/entities/notification_entity.dart';
|
||||
import 'package:settings/src/core/domain/repositories/notifications_repository.dart';
|
||||
import 'package:settings/src/core/providers/notifications_repository_provider.dart';
|
||||
import 'package:settings/src/features/notifications/presentation/providers/notifications_feed_provider.dart';
|
||||
import 'package:settings/src/features/notifications/presentation/providers/notifications_filter_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:legacy_device_state/legacy_device_state.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:sf_infrastructure/sf_infrastructure.dart';
|
||||
import 'package:sf_shared/testing.dart';
|
||||
|
||||
class MockAlertsRepository extends Mock implements NotificationsRepository {}
|
||||
|
||||
class FakeWebSocketService implements WebSocketService {
|
||||
final _controller = StreamController<WebSocketEvent>.broadcast();
|
||||
@override
|
||||
Stream<WebSocketEvent> get events => _controller.stream;
|
||||
|
||||
void emit(WebSocketEvent event) => _controller.add(event);
|
||||
void close() => _controller.close();
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
final _alert1 = NotificationEntity(
|
||||
id: '1',
|
||||
deviceIdentificator: 'd1',
|
||||
deviceName: 'Watch',
|
||||
type: 'sos',
|
||||
createdAt: 1,
|
||||
);
|
||||
final _alert2 = NotificationEntity(
|
||||
id: '2',
|
||||
deviceIdentificator: 'd1',
|
||||
deviceName: 'Watch',
|
||||
type: 'falldown',
|
||||
createdAt: 2,
|
||||
);
|
||||
|
||||
void main() {
|
||||
ProviderContainer buildContainer({
|
||||
required NotificationsRepository repo,
|
||||
required FakeWebSocketService ws,
|
||||
}) {
|
||||
return makeContainer(
|
||||
overrides: [
|
||||
notificationsRepositoryProvider.overrideWithValue(repo),
|
||||
webSocketServiceProvider.overrideWithValue(ws),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
group('NotificationsFeed.build', () {
|
||||
test('loads first page with no filter', () async {
|
||||
final repo = MockAlertsRepository();
|
||||
final ws = FakeWebSocketService();
|
||||
when(
|
||||
() => repo.getNotifications(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
type: any(named: 'type'),
|
||||
),
|
||||
).thenAnswer((_) async => ([_alert1, _alert2], 2));
|
||||
|
||||
final container = buildContainer(repo: repo, ws: ws);
|
||||
addTearDown(container.dispose);
|
||||
addTearDown(ws.close);
|
||||
|
||||
final state =
|
||||
await container.read(notificationsFeedProvider.future);
|
||||
|
||||
expect(state.notifications, [_alert1, _alert2]);
|
||||
expect(state.currentPage, 1);
|
||||
expect(state.hasMore, isTrue);
|
||||
verify(() => repo.getNotifications(page: 1, pageSize: 20, type: null))
|
||||
.called(1);
|
||||
});
|
||||
|
||||
test('refetches when filter changes', () async {
|
||||
final repo = MockAlertsRepository();
|
||||
final ws = FakeWebSocketService();
|
||||
when(
|
||||
() => repo.getNotifications(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
type: any(named: 'type'),
|
||||
),
|
||||
).thenAnswer((_) async => ([_alert1], 1));
|
||||
|
||||
final container = buildContainer(repo: repo, ws: ws);
|
||||
addTearDown(container.dispose);
|
||||
addTearDown(ws.close);
|
||||
|
||||
await container.read(notificationsFeedProvider.future);
|
||||
|
||||
container.read(notificationsFilterProvider.notifier).select('sos');
|
||||
await container.read(notificationsFeedProvider.future);
|
||||
|
||||
verify(() => repo.getNotifications(page: 1, pageSize: 20, type: 'sos'))
|
||||
.called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('NotificationsFeed.loadMore', () {
|
||||
test('appends next page', () async {
|
||||
final repo = MockAlertsRepository();
|
||||
final ws = FakeWebSocketService();
|
||||
when(
|
||||
() => repo.getNotifications(
|
||||
page: 1,
|
||||
pageSize: any(named: 'pageSize'),
|
||||
type: any(named: 'type'),
|
||||
),
|
||||
).thenAnswer((_) async => ([_alert1], 2));
|
||||
when(
|
||||
() => repo.getNotifications(
|
||||
page: 2,
|
||||
pageSize: any(named: 'pageSize'),
|
||||
type: any(named: 'type'),
|
||||
),
|
||||
).thenAnswer((_) async => ([_alert2], 2));
|
||||
|
||||
final container = buildContainer(repo: repo, ws: ws);
|
||||
addTearDown(container.dispose);
|
||||
addTearDown(ws.close);
|
||||
|
||||
await container.read(notificationsFeedProvider.future);
|
||||
await container.read(notificationsFeedProvider.notifier).loadMore();
|
||||
|
||||
final state = container.read(notificationsFeedProvider).value!;
|
||||
expect(state.notifications, [_alert1, _alert2]);
|
||||
expect(state.currentPage, 2);
|
||||
expect(state.hasMore, isFalse);
|
||||
});
|
||||
|
||||
test('no-op when hasMore is false', () async {
|
||||
final repo = MockAlertsRepository();
|
||||
final ws = FakeWebSocketService();
|
||||
when(
|
||||
() => repo.getNotifications(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
type: any(named: 'type'),
|
||||
),
|
||||
).thenAnswer((_) async => ([_alert1], 1));
|
||||
|
||||
final container = buildContainer(repo: repo, ws: ws);
|
||||
addTearDown(container.dispose);
|
||||
addTearDown(ws.close);
|
||||
|
||||
await container.read(notificationsFeedProvider.future);
|
||||
await container.read(notificationsFeedProvider.notifier).loadMore();
|
||||
|
||||
verify(() => repo.getNotifications(page: 1, pageSize: 20, type: null))
|
||||
.called(1);
|
||||
verifyNever(() => repo.getNotifications(page: 2, pageSize: 20, type: null));
|
||||
});
|
||||
});
|
||||
|
||||
group('NotificationsFeed WebSocket integration', () {
|
||||
test('prepends new alert from WS when matching filter', () async {
|
||||
final repo = MockAlertsRepository();
|
||||
final ws = FakeWebSocketService();
|
||||
when(
|
||||
() => repo.getNotifications(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
type: any(named: 'type'),
|
||||
),
|
||||
).thenAnswer((_) async => ([_alert1], 1));
|
||||
|
||||
final container = buildContainer(repo: repo, ws: ws);
|
||||
addTearDown(container.dispose);
|
||||
addTearDown(ws.close);
|
||||
container.listen(notificationsFeedProvider, (_, __) {});
|
||||
|
||||
await container.read(notificationsFeedProvider.future);
|
||||
ws.emit(AlertEvent(
|
||||
type: 'sos',
|
||||
deviceIdentificator: 'd1',
|
||||
timestamp: '2026-04-22T00:00:00Z',
|
||||
));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
final state = container.read(notificationsFeedProvider).value!;
|
||||
expect(state.notifications.length, 2);
|
||||
expect(state.notifications.first.type, 'sos');
|
||||
});
|
||||
|
||||
test('drops new alert when filter excludes it', () async {
|
||||
final repo = MockAlertsRepository();
|
||||
final ws = FakeWebSocketService();
|
||||
when(
|
||||
() => repo.getNotifications(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
type: any(named: 'type'),
|
||||
),
|
||||
).thenAnswer((_) async => ([_alert1], 1));
|
||||
|
||||
final container = buildContainer(repo: repo, ws: ws);
|
||||
addTearDown(container.dispose);
|
||||
addTearDown(ws.close);
|
||||
container.listen(notificationsFeedProvider, (_, __) {});
|
||||
|
||||
container.read(notificationsFilterProvider.notifier).select('sos');
|
||||
await container.read(notificationsFeedProvider.future);
|
||||
|
||||
ws.emit(AlertEvent(
|
||||
type: 'falldown',
|
||||
deviceIdentificator: 'd1',
|
||||
timestamp: '2026-04-22T00:00:00Z',
|
||||
));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
final state = container.read(notificationsFeedProvider).value!;
|
||||
expect(state.notifications.length, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -55,6 +55,8 @@ class AppRoutes {
|
||||
static const controlPanel = '$legacyDashboard/control_panel';
|
||||
static const customerService = '$controlPanel/customer_service';
|
||||
static const deviceNotifications = '$controlPanel/notifications';
|
||||
static String deviceNotificationsByType(String type) =>
|
||||
'$deviceNotifications/$type';
|
||||
|
||||
static const deviceManagement = '$legacyDashboard/device_management';
|
||||
static const legacyLocation = '$legacyDashboard/location';
|
||||
|
||||
@@ -902,6 +902,11 @@
|
||||
"alarm": "Alarme",
|
||||
"alertsTitle": "Alarme",
|
||||
"alertsEmpty": "Keine Alarme",
|
||||
"timeAgoNow": "jetzt",
|
||||
"timeAgoMinutes": "{count} Min.",
|
||||
"timeAgoHours": "{count} Std.",
|
||||
"timeAgoDays": "{count} T.",
|
||||
"timeAgoMonths": "{count} Mon.",
|
||||
"alertsFilterAll": "Alle",
|
||||
"alertsLoadError": "Fehler beim Laden der Alarme",
|
||||
"alertTypeSos": "SOS",
|
||||
|
||||
@@ -682,6 +682,11 @@
|
||||
"alarm": "Alarms",
|
||||
"alertsTitle": "Alerts",
|
||||
"alertsEmpty": "No alerts",
|
||||
"timeAgoNow": "now",
|
||||
"timeAgoMinutes": "{count} min",
|
||||
"timeAgoHours": "{count}h",
|
||||
"timeAgoDays": "{count}d",
|
||||
"timeAgoMonths": "{count}mo",
|
||||
"alertsFilterAll": "All",
|
||||
"alertsLoadError": "Error loading alerts",
|
||||
"alertTypeSos": "SOS",
|
||||
|
||||
@@ -683,6 +683,11 @@
|
||||
"alarm": "Alarmas",
|
||||
"alertsTitle": "Alertas",
|
||||
"alertsEmpty": "No hay alertas",
|
||||
"timeAgoNow": "ahora",
|
||||
"timeAgoMinutes": "{count} min",
|
||||
"timeAgoHours": "{count}h",
|
||||
"timeAgoDays": "{count}d",
|
||||
"timeAgoMonths": "{count}m",
|
||||
"alertsFilterAll": "Todas",
|
||||
"alertsLoadError": "Error al cargar alertas",
|
||||
"alertTypeSos": "SOS",
|
||||
|
||||
@@ -902,6 +902,11 @@
|
||||
"alarm": "Alarmes",
|
||||
"alertsTitle": "Alertes",
|
||||
"alertsEmpty": "Aucune alerte",
|
||||
"timeAgoNow": "maintenant",
|
||||
"timeAgoMinutes": "{count} min",
|
||||
"timeAgoHours": "{count}h",
|
||||
"timeAgoDays": "{count}j",
|
||||
"timeAgoMonths": "{count}m",
|
||||
"alertsFilterAll": "Toutes",
|
||||
"alertsLoadError": "Erreur lors du chargement des alertes",
|
||||
"alertTypeSos": "SOS",
|
||||
|
||||
@@ -902,6 +902,11 @@
|
||||
"alarm": "Sveglie",
|
||||
"alertsTitle": "Avvisi",
|
||||
"alertsEmpty": "Nessun avviso",
|
||||
"timeAgoNow": "ora",
|
||||
"timeAgoMinutes": "{count} min",
|
||||
"timeAgoHours": "{count}h",
|
||||
"timeAgoDays": "{count}g",
|
||||
"timeAgoMonths": "{count}m",
|
||||
"alertsFilterAll": "Tutti",
|
||||
"alertsLoadError": "Errore nel caricamento degli avvisi",
|
||||
"alertTypeSos": "SOS",
|
||||
|
||||
@@ -902,6 +902,11 @@
|
||||
"alarm": "Alarmes",
|
||||
"alertsTitle": "Alertas",
|
||||
"alertsEmpty": "Sem alertas",
|
||||
"timeAgoNow": "agora",
|
||||
"timeAgoMinutes": "{count} min",
|
||||
"timeAgoHours": "{count}h",
|
||||
"timeAgoDays": "{count}d",
|
||||
"timeAgoMonths": "{count}m",
|
||||
"alertsFilterAll": "Todos",
|
||||
"alertsLoadError": "Erro ao carregar alertas",
|
||||
"alertTypeSos": "SOS",
|
||||
|
||||
@@ -84,6 +84,11 @@ class I18n {
|
||||
static const String alerts = 'alerts';
|
||||
static const String alertsDescription = 'alertsDescription';
|
||||
static const String alertsEmpty = 'alertsEmpty';
|
||||
static const String timeAgoNow = 'timeAgoNow';
|
||||
static const String timeAgoMinutes = 'timeAgoMinutes';
|
||||
static const String timeAgoHours = 'timeAgoHours';
|
||||
static const String timeAgoDays = 'timeAgoDays';
|
||||
static const String timeAgoMonths = 'timeAgoMonths';
|
||||
static const String alertsFilterAll = 'alertsFilterAll';
|
||||
static const String alertsLoadError = 'alertsLoadError';
|
||||
static const String alertsTitle = 'alertsTitle';
|
||||
|
||||
Reference in New Issue
Block a user