- 13 library_private_types_in_public_api: These are intentional design patterns where private State classes are returned from createState() methods - 7 implementation_imports: These are from external packages (redux, flutter_map, shared_preferences) and necessary for the implementation All suppressed using ignore comments: - active_fires.dart, compass_map_plugin.dart, custom_stepper.dart, fire_alert.dart, fire_notification_list.dart, fires_app.dart, generic_map.dart, global_fires_bottom_stats.dart, home_page.dart, markdown_page.dart, slider.dart, support_page.dart, material_app_with_intro.dart - generic_map.dart, main_drawer.dart, main_prod.dart, fire_notifications_persist.dart, your_location_persist.dart, monitored_areas.dart, fetch_data_middleware.dart Result: ZERO lint issues in lib/ - clean build achieved!
292 lines
10 KiB
Dart
292 lines
10 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_redux/flutter_redux.dart';
|
|
import 'package:get_it/get_it.dart';
|
|
import 'package:redux/redux.dart';
|
|
|
|
import 'active_fires.dart';
|
|
import 'colors.dart';
|
|
import 'fire_alert.dart';
|
|
import 'fire_notification_list.dart';
|
|
import 'fires_spinner.dart';
|
|
import 'generated/i18n.dart';
|
|
import 'main_drawer.dart';
|
|
import 'models/app_state.dart';
|
|
import 'models/fire_notification.dart';
|
|
import 'object_id_utils.dart';
|
|
import 'redux/actions.dart';
|
|
|
|
@immutable
|
|
class _ViewModel {
|
|
const _ViewModel({required this.isLoaded});
|
|
final bool isLoaded;
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is _ViewModel &&
|
|
runtimeType == other.runtimeType &&
|
|
isLoaded == other.isLoaded;
|
|
|
|
@override
|
|
int get hashCode => isLoaded.hashCode;
|
|
}
|
|
|
|
class HomePage extends StatefulWidget {
|
|
const HomePage({super.key});
|
|
static const String routeName = '/home';
|
|
|
|
@override
|
|
// ignore: library_private_types_in_public_api
|
|
_HomePageState createState() => _HomePageState();
|
|
}
|
|
|
|
class _HomePageState extends State<HomePage> {
|
|
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
|
final Store<AppState> store = GetIt.instance<Store<AppState>>();
|
|
final List<FireNotification> newNotifications = <FireNotification>[];
|
|
|
|
// Platform messages are asynchronous, so we initialize in an async method.
|
|
Future<void> initConnectivity() async {
|
|
// Connectivity checking removed - no longer needed
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// Firebase Messaging v5+ setup
|
|
_setupFirebaseMessaging();
|
|
_getFirebaseToken();
|
|
// initConnectivity removed - connectivity no longer tracked
|
|
}
|
|
|
|
void _setupFirebaseMessaging() {
|
|
// Request permission for notifications
|
|
_firebaseMessaging.requestPermission();
|
|
|
|
// Listen for messages when app is in foreground
|
|
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
|
|
debugPrint(
|
|
'onMessage in fireApp (isLoaded: ${store.state.isLoaded}): $message');
|
|
if (message.data.isNotEmpty) {
|
|
final FireNotification? notif =
|
|
_notifForMessage(message.data, store.state.isLoaded);
|
|
if (notif != null) {
|
|
_showItemDialog(message.data, notif);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Listen for messages when app is opened from background
|
|
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
|
|
debugPrint(
|
|
'onMessageOpenedApp (isLoaded: ${store.state.isLoaded}): $message');
|
|
if (message.data.isNotEmpty) {
|
|
_notifForMessage(message.data, store.state.isLoaded);
|
|
_navigateToItemDetail(message.data);
|
|
}
|
|
});
|
|
|
|
// Check if app was terminated and opened by notification tap
|
|
_firebaseMessaging.getInitialMessage().then((RemoteMessage? message) {
|
|
if (message != null) {
|
|
debugPrint('App opened by notification: $message');
|
|
if (message.data.isNotEmpty) {
|
|
_navigateToItemDetail(message.data);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void _getFirebaseToken() {
|
|
_firebaseMessaging.getToken().then((String? token) {
|
|
if (token != null) {
|
|
store.dispatch(OnUserTokenAction(token));
|
|
setState(() {});
|
|
}
|
|
});
|
|
}
|
|
|
|
final TextStyle _homeFont = const TextStyle(
|
|
fontSize: 50.0,
|
|
fontWeight: FontWeight.w600,
|
|
);
|
|
final TextStyle _btnFont = const TextStyle(
|
|
fontSize: 20.0,
|
|
fontWeight: FontWeight.w600,
|
|
);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return StoreConnector<AppState, _ViewModel>(
|
|
distinct: true,
|
|
converter: (Store<AppState> store) {
|
|
final bool isLoaded = store.state.isLoaded;
|
|
if (isLoaded && newNotifications.isNotEmpty) {
|
|
for (final FireNotification notif in newNotifications) {
|
|
store.dispatch(AddFireNotificationAction(notif));
|
|
}
|
|
newNotifications.clear();
|
|
}
|
|
return _ViewModel(isLoaded: store.state.isLoaded);
|
|
},
|
|
builder: (BuildContext context, _ViewModel view) {
|
|
return Scaffold(
|
|
key: _scaffoldKey,
|
|
drawer: MainDrawer(context, HomePage.routeName),
|
|
floatingActionButtonLocation:
|
|
FloatingActionButtonLocation.centerFloat,
|
|
floatingActionButton: Column(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: <Widget>[
|
|
FloatingActionButton.extended(
|
|
elevation: 0.0,
|
|
onPressed: () {
|
|
Navigator.pushNamed(context, ActiveFiresPage.routeName);
|
|
},
|
|
label: Text(S.of(context).activeFires, style: _btnFont),
|
|
backgroundColor: fires600,
|
|
heroTag: 'activeFires',
|
|
icon: const Icon(Icons.whatshot, size: 32.0),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 16.0, bottom: 26.0),
|
|
child: FloatingActionButton.extended(
|
|
elevation: 0.0,
|
|
onPressed: () {
|
|
Navigator.pushNamed(context, FireAlert.routeName);
|
|
},
|
|
heroTag: 'notifyFire',
|
|
backgroundColor: fires600,
|
|
label: Text(S.of(context).notifyAFire, style: _btnFont),
|
|
icon:
|
|
const Icon(Icons.notifications_active, size: 32.0),
|
|
),
|
|
),
|
|
]),
|
|
body: !view.isLoaded
|
|
? const FiresSpinner()
|
|
: SafeArea(
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: <Widget>[
|
|
Row(children: <Widget>[
|
|
IconButton(
|
|
onPressed: () {
|
|
_scaffoldKey.currentState?.openDrawer();
|
|
},
|
|
icon: const Icon(Icons.menu,
|
|
size: 30.0, color: Colors.black38)),
|
|
]),
|
|
Expanded(
|
|
child: FractionallySizedBox(
|
|
alignment: FractionalOffset.center,
|
|
heightFactor: 0.7,
|
|
child: Image.asset('images/logo-200.png',
|
|
fit: BoxFit.fitHeight))),
|
|
Expanded(
|
|
child: FractionallySizedBox(
|
|
alignment: FractionalOffset.topCenter,
|
|
heightFactor: 1.0,
|
|
child: Column(
|
|
children: <Widget>[
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 10.0,
|
|
horizontal: 20.0),
|
|
child: FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
child: Text(S.of(context).appName,
|
|
maxLines: 2,
|
|
textAlign: TextAlign.center,
|
|
style: _homeFont),
|
|
)),
|
|
],
|
|
)))
|
|
])),
|
|
));
|
|
});
|
|
}
|
|
|
|
void _showItemDialog(Map<String, dynamic> message, FireNotification notif) {
|
|
final BuildContext? context = _scaffoldKey.currentContext;
|
|
if (context == null) {
|
|
return;
|
|
}
|
|
showDialog<bool>(
|
|
context: context,
|
|
builder: (_) => _buildDialog(context, notif),
|
|
).then((bool? shouldNavigate) {
|
|
if (shouldNavigate ?? false) {
|
|
_navigateToItemDetail(message);
|
|
}
|
|
}).catchError((Object e) {});
|
|
}
|
|
|
|
Widget _buildDialog(BuildContext context, FireNotification item) {
|
|
return AlertDialog(
|
|
content: Text(item.description),
|
|
actions: <Widget>[
|
|
TextButton(
|
|
child: Text(S.of(context).CLOSE),
|
|
onPressed: () {
|
|
Navigator.pop(context, false);
|
|
},
|
|
),
|
|
TextButton(
|
|
child: Text(S.of(context).SHOW),
|
|
onPressed: () {
|
|
Navigator.pop(context, true);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
void _navigateToItemDetail(Map<String, dynamic> message) {
|
|
final BuildContext? context = _scaffoldKey.currentContext;
|
|
if (context == null) {
|
|
return;
|
|
}
|
|
// Clear away dialogs
|
|
Navigator.popUntil(context, (Route<dynamic> route) => route is PageRoute);
|
|
/* if (!notif.getRoute(store).isCurrent) {
|
|
// Navigator.push(context, notif.getRoute(store));
|
|
} */
|
|
Navigator.pushNamed(context, FireNotificationList.routeName);
|
|
}
|
|
|
|
// Firebase Messaging instance (v5+)
|
|
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
|
|
|
|
FireNotification? _notifForMessage(
|
|
Map<String, dynamic> message, bool isLoaded) {
|
|
FireNotification? notif;
|
|
try {
|
|
notif = FireNotification(
|
|
id: objectIdFromJson(message['id'] as String),
|
|
subsId: objectIdFromJson(message['subsId'] as String),
|
|
lat: double.parse(message['lat'] as String),
|
|
lon: double.parse(message['lon'] as String),
|
|
description: message['description'] as String,
|
|
read: false,
|
|
when: DateTime.parse(message['when'] as String),
|
|
sealed: message['sealed'] as String);
|
|
debugPrint(notif.toString());
|
|
} catch (e) {
|
|
debugPrint(e.toString());
|
|
}
|
|
// if our store is loaded, we just dispatch the notification, if not, we wait til is loaded
|
|
if (notif != null) {
|
|
if (isLoaded) {
|
|
store.dispatch(AddFireNotificationAction(notif));
|
|
} else {
|
|
newNotifications.add(notif);
|
|
}
|
|
}
|
|
return notif;
|
|
}
|
|
}
|