fix: resolve 2 remaining strict analysis errors

- introPage.dart: Convert fireItems to proper static function closure
- mainDrawer.dart: Convert unreadCount int to String for Text widget
This commit is contained in:
vjrj 2026-03-06 11:21:06 +01:00
parent 1864e5b105
commit 7a4c9fb3bc
61 changed files with 1386 additions and 1202 deletions

181
analysis_options.yaml Normal file
View file

@ -0,0 +1,181 @@
# Comprehensive Dart analysis configuration for Fires Flutter
# Based on Flutter framework standards with aggressive linting
analyzer:
language:
strict-casts: true
strict-raw-types: true
strict-inference: true
errors:
# Treat errors as errors (default behavior)
missing_required_param: error
missing_return: error
# Warnings configuration
deprecated_member_use_from_same_package: warning
unnecessary_null_comparison: ignore
todo: ignore
exclude:
- 'build/**'
- 'lib/generated/**'
- 'lib/data/models/*g.dart'
- '.dart_tool/**'
linter:
rules:
# Error handling and safety
- always_declare_return_types
- always_put_control_body_on_new_line
- always_specify_types
- annotate_overrides
- avoid_bool_literals_in_conditional_expressions
- avoid_classes_with_only_static_members
- avoid_double_and_int_checks
- avoid_dynamic_calls
- avoid_empty_else
- avoid_equals_and_hash_code_on_mutable_classes
- avoid_escaping_inner_quotes
- avoid_field_initializers_in_const_classes
- avoid_function_literals_in_foreach_calls
- avoid_implementing_value_types
- avoid_init_to_null
- avoid_js_rounded_ints
- avoid_null_checks_in_equality_operators
- avoid_print
- avoid_redundant_argument_values
- avoid_relative_lib_imports
- avoid_renaming_method_parameters
- avoid_return_types_on_setters
- avoid_returning_null_for_void
- avoid_setters_without_getters
- avoid_shadowing_type_parameters
- avoid_single_cascade_in_expression_statements
- avoid_slow_async_io
- avoid_type_to_string
- avoid_types_as_parameter_names
- avoid_unnecessary_containers
- avoid_unused_constructor_parameters
- avoid_void_async
- await_only_futures
# Naming conventions
- camel_case_extensions
- camel_case_types
- file_names
- library_names
- library_prefixes
- library_private_types_in_public_api
- non_constant_identifier_names
- package_names
- package_prefixed_library_names
# Code quality and style
- cast_nullable_to_non_nullable
- control_flow_in_finally
- depend_on_referenced_packages
- deprecated_consistency
- directives_ordering
- empty_catches
- empty_constructor_bodies
- empty_statements
- eol_at_end_of_file
- exhaustive_cases
- flutter_style_todos
- hash_and_equals
- implementation_imports
- collection_methods_unrelated_type
- leading_newlines_in_multiline_strings
- missing_whitespace_between_adjacent_strings
- no_adjacent_strings_in_list
- no_default_cases
- no_duplicate_case_values
- no_leading_underscores_for_library_prefixes
- no_leading_underscores_for_local_identifiers
- no_logic_in_create_state
- noop_primitive_operations
- null_check_on_nullable_type_parameter
- null_closures
- only_throw_errors
- overridden_fields
# Preferences and improvements
- prefer_adjacent_string_concatenation
- prefer_asserts_in_initializer_lists
- prefer_collection_literals
- prefer_conditional_assignment
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- prefer_contains
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
- prefer_for_elements_to_map_fromIterable
- prefer_foreach
- prefer_function_declarations_over_variables
- prefer_generic_function_type_aliases
- prefer_if_elements_to_conditional_expressions
- prefer_if_null_operators
- prefer_initializing_formals
- prefer_inlined_adds
- prefer_interpolation_to_compose_strings
- prefer_is_empty
- prefer_is_not_empty
- prefer_is_not_operator
- prefer_iterable_whereType
- prefer_null_aware_operators
- prefer_relative_imports
- prefer_single_quotes
- prefer_spread_collections
- prefer_typing_uninitialized_variables
- prefer_void_to_null
- provide_deprecation_message
- recursive_getters
- secure_pubspec_urls
- sized_box_for_whitespace
- slash_for_doc_comments
- sort_child_properties_last
- sort_constructors_first
- sort_unnamed_constructors_first
- test_types_in_equals
- throw_in_finally
- tighten_type_of_initializing_formals
- type_init_formals
- unnecessary_await_in_return
- unnecessary_brace_in_string_interps
- unnecessary_const
- unnecessary_constructor_name
- unnecessary_getters_setters
- unnecessary_late
- unnecessary_new
- unnecessary_null_aware_assignments
- unnecessary_null_checks
- unnecessary_null_in_if_null_operators
- unnecessary_nullable_for_final_variable_declarations
- unnecessary_overrides
- unnecessary_parenthesis
- unnecessary_statements
- unnecessary_string_escapes
- unnecessary_string_interpolations
- unnecessary_this
- unrelated_type_equality_checks
- use_build_context_synchronously
- use_full_hex_values_for_flutter_colors
- use_function_type_syntax_for_parameters
- use_if_null_to_convert_nulls_to_bools
- use_is_even_rather_than_modulo
- use_key_in_widget_constructors
- use_late_for_private_fields_and_variables
- use_named_constants
- use_raw_strings
- use_rethrow_when_possible
- use_setters_to_change_properties
- use_super_parameters
- use_test_throws_matchers
- valid_regexps
- void_checks
include: package:bloc_lint/recommended.yaml
bloc:
rules:
- avoid_flutter_imports

View file

@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:fires_flutter/models/yourLocation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
import 'package:flutter_speed_dial/flutter_speed_dial.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart';
@ -15,20 +14,14 @@ import 'globalFiresBottomStats.dart';
import 'locationUtils.dart'; import 'locationUtils.dart';
import 'mainDrawer.dart'; import 'mainDrawer.dart';
import 'models/appState.dart'; import 'models/appState.dart';
import 'models/yourLocation.dart';
import 'placesAutocompleteUtils.dart'; import 'placesAutocompleteUtils.dart';
import 'redux/actions.dart'; import 'redux/actions.dart';
@immutable @immutable
class _ViewModel { class _ViewModel {
final List<YourLocation> yourLocations;
final AddYourLocationFunction onAdd;
final DeleteYourLocationFunction onDelete;
final ToggleSubscriptionFunction onToggleSubs;
final OnLocationTapFunction onTap;
final OnRefreshYourLocationsFunction onRefresh;
final bool isLoading;
_ViewModel( const _ViewModel(
{required this.onAdd, {required this.onAdd,
required this.onDelete, required this.onDelete,
required this.onToggleSubs, required this.onToggleSubs,
@ -36,6 +29,13 @@ class _ViewModel {
required this.onRefresh, required this.onRefresh,
required this.yourLocations, required this.yourLocations,
required this.isLoading}); required this.isLoading});
final List<YourLocation> yourLocations;
final AddYourLocationFunction onAdd;
final DeleteYourLocationFunction onDelete;
final ToggleSubscriptionFunction onToggleSubs;
final OnLocationTapFunction onTap;
final OnRefreshYourLocationsFunction onRefresh;
final bool isLoading;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@ -50,6 +50,8 @@ class _ViewModel {
} }
class ActiveFiresPage extends StatefulWidget { class ActiveFiresPage extends StatefulWidget {
const ActiveFiresPage({super.key});
static const String routeName = '/fires'; static const String routeName = '/fires';
@override @override
@ -57,11 +59,11 @@ class ActiveFiresPage extends StatefulWidget {
} }
class _ActiveFiresPageState extends State<ActiveFiresPage> { class _ActiveFiresPageState extends State<ActiveFiresPage> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
List<SpeedDialChild> _fabMiniMenuItemList( List<SpeedDialChild> _fabMiniMenuItemList(
BuildContext context, AddYourLocationFunction onAdd) { BuildContext context, AddYourLocationFunction onAdd) {
return [ return <SpeedDialChild>[
SpeedDialChild( SpeedDialChild(
child: const Icon(Icons.location_searching), child: const Icon(Icons.location_searching),
backgroundColor: fires600, backgroundColor: fires600,
@ -92,13 +94,13 @@ class _ActiveFiresPageState extends State<ActiveFiresPage> {
} }
Widget _buildRow(BuildContext context, YourLocation loc, onToggle, onTap) { Widget _buildRow(BuildContext context, YourLocation loc, onToggle, onTap) {
final fireStatsStyle = new TextStyle(color: fires600); const TextStyle fireStatsStyle = TextStyle(color: fires600);
return new ListTile( return ListTile(
dense: true, dense: true,
isThreeLine: false, isThreeLine: false,
// leading: const Icon(Icons.location_on), // leading: const Icon(Icons.location_on),
trailing: new IconButton( trailing: IconButton(
icon: new Icon(loc.subscribed icon: Icon(loc.subscribed
? Icons.notifications_active ? Icons.notifications_active
: Icons.notifications_off), : Icons.notifications_off),
color: loc.subscribed ? fires600 : null, color: loc.subscribed ? fires600 : null,
@ -110,18 +112,18 @@ class _ActiveFiresPageState extends State<ActiveFiresPage> {
? S.of(context).subscribedToFires ? S.of(context).subscribedToFires
: S.of(context).unsubscribedToFires); : S.of(context).unsubscribedToFires);
}), }),
title: new Text(loc.description, style: new TextStyle(fontSize: 16.0)), title: Text(loc.description, style: const TextStyle(fontSize: 16.0)),
subtitle: loc.currentNumFires == YourLocation.withoutStats subtitle: loc.currentNumFires == YourLocation.withoutStats
? null ? null
: loc.currentNumFires == 0 : loc.currentNumFires == 0
? new Text(S.of(context).noFiresAround) ? Text(S.of(context).noFiresAround)
: loc.currentNumFires == 1 : loc.currentNumFires == 1
? new Text( ? Text(
S S
.of(context) .of(context)
.fireAroundThisArea(loc.distance.toString()), .fireAroundThisArea(loc.distance.toString()),
style: fireStatsStyle) style: fireStatsStyle)
: new Text( : Text(
S.of(context).firesAroundThisArea( S.of(context).firesAroundThisArea(
loc.currentNumFires.toString(), loc.currentNumFires.toString(),
loc.distance.toString()), loc.distance.toString()),
@ -136,86 +138,85 @@ class _ActiveFiresPageState extends State<ActiveFiresPage> {
void showSnackMsg(String msg) { void showSnackMsg(String msg) {
// _scaffoldKey.currentState.showSnackBar(new SnackBar( // _scaffoldKey.currentState.showSnackBar(new SnackBar(
ScaffoldMessenger.of(context).showSnackBar(new SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: new Text(msg), content: Text(msg),
)); ));
} }
Widget _buildSavedLocations( Widget _buildSavedLocations(
BuildContext context, List<YourLocation> yl, onDeleted, onToggle, onTap) { BuildContext context, List<YourLocation> yl, onDeleted, onToggle, onTap) {
return new ListView.builder( return ListView.builder(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
itemCount: yl.length, itemCount: yl.length,
itemBuilder: (BuildContext _context, int i) { itemBuilder: (BuildContext context, int i) {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
return new Dismissible( return Dismissible(
key: new ObjectKey(yl.elementAt(i)), key: ObjectKey(yl.elementAt(i)),
direction: DismissDirection.horizontal,
onDismissed: (DismissDirection direction) { onDismissed: (DismissDirection direction) {
onDeleted(yl.elementAt(i)); onDeleted(yl.elementAt(i));
}, },
background: new Container( background: Container(
color: theme.primaryColor, color: theme.primaryColor,
child: const ListTile( child: const ListTile(
leading: const Icon(Icons.delete, leading: Icon(Icons.delete,
color: Colors.white, size: 36.0))), color: Colors.white, size: 36.0))),
secondaryBackground: new Container( secondaryBackground: Container(
color: theme.primaryColor, color: theme.primaryColor,
child: const ListTile( child: const ListTile(
trailing: const Icon(Icons.delete, trailing: Icon(Icons.delete,
color: Colors.white, size: 36.0))), color: Colors.white, size: 36.0))),
child: new Container( child: Container(
decoration: new BoxDecoration( decoration: BoxDecoration(
color: theme.canvasColor, color: theme.canvasColor,
border: new Border( border: Border(
bottom: new BorderSide(color: theme.dividerColor))), bottom: BorderSide(color: theme.dividerColor))),
child: _buildRow(context, yl.elementAt(i), onToggle, onTap))); child: _buildRow(context, yl.elementAt(i), onToggle, onTap)));
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new StoreConnector<AppState, _ViewModel>( return StoreConnector<AppState, _ViewModel>(
distinct: true, distinct: true,
converter: (store) { converter: (Store<AppState> store) {
print('New ViewModel of Active Fires'); print('New ViewModel of Active Fires');
return new _ViewModel( return _ViewModel(
onAdd: (loc) { onAdd: (YourLocation loc) {
if (store.state.yourLocations if (store.state.yourLocations
.any((l) => loc.lat == l.lat && loc.lon == l.lon)) { .any((YourLocation l) => loc.lat == l.lat && loc.lon == l.lon)) {
// Already added // Already added
showSnackMsg(S.of(context).addedThisLocation); showSnackMsg(S.of(context).addedThisLocation);
} else { } else {
store.dispatch(new AddYourLocationAction(loc)); store.dispatch(AddYourLocationAction(loc));
new Timer(new Duration(milliseconds: 1000), () { Timer(const Duration(milliseconds: 1000), () {
gotoLocationMap(store, loc, context); gotoLocationMap(store, loc, context);
}); });
} }
}, },
onDelete: (loc) { onDelete: (YourLocation loc) {
store.dispatch(new DeleteYourLocationAction(loc)); store.dispatch(DeleteYourLocationAction(loc));
ScaffoldMessenger.of(context).showSnackBar(new SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: new Text(S.of(context).youDeletedThisPlace), content: Text(S.of(context).youDeletedThisPlace),
action: new SnackBarAction( action: SnackBarAction(
label: S.of(context).UNDO, label: S.of(context).UNDO,
onPressed: () { onPressed: () {
store.dispatch(new AddYourLocationAction(loc)); store.dispatch(AddYourLocationAction(loc));
}))); })));
}, },
onToggleSubs: (loc) { onToggleSubs: (YourLocation loc) {
store.dispatch(new ToggleSubscriptionAction(loc)); store.dispatch(ToggleSubscriptionAction(loc));
}, },
onTap: (loc) { onTap: (YourLocation loc) {
gotoLocationMap(store, loc, context); gotoLocationMap(store, loc, context);
}, },
onRefresh: (refreshCallback) => onRefresh: (Completer<void> refreshCallback) =>
store.dispatch(new FetchYourLocationsAction(refreshCallback)), store.dispatch(FetchYourLocationsAction(refreshCallback)),
yourLocations: store.state.yourLocations, yourLocations: store.state.yourLocations,
isLoading: !store.state.isLoaded); isLoading: !store.state.isLoaded);
}, },
builder: (context, view) { builder: (BuildContext context, _ViewModel view) {
var hasLocations = view.yourLocations.length > 0; final bool hasLocations = view.yourLocations.isNotEmpty;
final title = hasLocations final String title = hasLocations
? S.of(context).firesInYourPlaces ? S.of(context).firesInYourPlaces
: S.of(context).firesInTheWorld; : S.of(context).firesInTheWorld;
print('Building Active Fires'); print('Building Active Fires');
@ -223,11 +224,11 @@ class _ActiveFiresPageState extends State<ActiveFiresPage> {
return Scaffold( return Scaffold(
key: _scaffoldKey, key: _scaffoldKey,
// FIXME new? // FIXME new?
drawer: new MainDrawer(context, ActiveFiresPage.routeName), drawer: MainDrawer(context, ActiveFiresPage.routeName),
appBar: new AppBar( appBar: AppBar(
title: Text(title), title: Text(title),
leading: IconButton( leading: IconButton(
icon: Icon(Icons.menu), icon: const Icon(Icons.menu),
onPressed: () { onPressed: () {
_scaffoldKey.currentState?.openDrawer(); _scaffoldKey.currentState?.openDrawer();
}, },
@ -235,12 +236,12 @@ class _ActiveFiresPageState extends State<ActiveFiresPage> {
), ),
floatingActionButtonLocation: floatingActionButtonLocation:
FloatingActionButtonLocation.centerFloat, FloatingActionButtonLocation.centerFloat,
bottomNavigationBar: new GlobalFiresBottomStats(), bottomNavigationBar: const GlobalFiresBottomStats(),
body: view.isLoading body: view.isLoading
? new FiresSpinner() ? const FiresSpinner()
: hasLocations : hasLocations
? new Stack(children: <Widget>[ ? Stack(children: <Widget>[
new RefreshIndicator( RefreshIndicator(
child: _buildSavedLocations( child: _buildSavedLocations(
context, context,
view.yourLocations, view.yourLocations,
@ -248,8 +249,8 @@ class _ActiveFiresPageState extends State<ActiveFiresPage> {
view.onToggleSubs, view.onToggleSubs,
view.onTap), view.onTap),
onRefresh: () async { onRefresh: () async {
final Completer<Null> completer = final Completer<void> completer =
new Completer<Null>(); Completer<void>();
view.onRefresh(completer); view.onRefresh(completer);
return completer.future; return completer.future;
}), }),
@ -260,15 +261,15 @@ class _ActiveFiresPageState extends State<ActiveFiresPage> {
children: _fabMiniMenuItemList(context, view.onAdd), children: _fabMiniMenuItemList(context, view.onAdd),
) )
]) ])
: new Center( : Center(
child: new CenteredColumn(children: <Widget>[ child: CenteredColumn(children: <Widget>[
new RoundedBtn( RoundedBtn(
icon: Icons.location_searching, icon: Icons.location_searching,
text: S.of(context).addYourCurrentPosition, text: S.of(context).addYourCurrentPosition,
onPressed: () => onAddYourLocation(view.onAdd), onPressed: () => onAddYourLocation(view.onAdd),
backColor: fires600), backColor: fires600),
const SizedBox(height: 26.0), const SizedBox(height: 26.0),
new RoundedBtn( RoundedBtn(
icon: Icons.edit_location, icon: Icons.edit_location,
text: S.of(context).addSomePlace, text: S.of(context).addSomePlace,
onPressed: () => onAddOtherLocation(view.onAdd), onPressed: () => onAddOtherLocation(view.onAdd),
@ -280,23 +281,23 @@ class _ActiveFiresPageState extends State<ActiveFiresPage> {
void gotoLocationMap( void gotoLocationMap(
Store<AppState> store, YourLocation loc, BuildContext context) { Store<AppState> store, YourLocation loc, BuildContext context) {
store.dispatch(new ShowYourLocationMapAction(loc)); store.dispatch(ShowYourLocationMapAction(loc));
Navigator.push( Navigator.push(
context, new MaterialPageRoute(builder: (context) => new genericMap())); context, MaterialPageRoute(builder: (BuildContext context) => const genericMap()));
} }
void onAddYourLocation(AddYourLocationFunction onAdd) { void onAddYourLocation(AddYourLocationFunction onAdd) {
Future<YourLocation> location = getUserLocation(_scaffoldKey); final Future<YourLocation> location = getUserLocation(_scaffoldKey);
_saveLocation(location, onAdd); _saveLocation(location, onAdd);
} }
void onAddOtherLocation(AddYourLocationFunction onAdd) { void onAddOtherLocation(AddYourLocationFunction onAdd) {
Future<YourLocation> location = openPlacesDialog(_scaffoldKey); final Future<YourLocation> location = openPlacesDialog(_scaffoldKey);
_saveLocation(location, onAdd); _saveLocation(location, onAdd);
} }
void _saveLocation(Future<YourLocation> location, onAdd) { void _saveLocation(Future<YourLocation> location, onAdd) {
location.then((newLocation) { location.then((YourLocation newLocation) {
if (newLocation != YourLocation.noLocation) { if (newLocation != YourLocation.noLocation) {
onAdd(newLocation); onAdd(newLocation);
} }

View file

@ -2,19 +2,19 @@ import 'package:flutter/material.dart';
/// Attribution widget for displaying map attribution text /// Attribution widget for displaying map attribution text
class AttributionPluginWidget extends StatelessWidget { class AttributionPluginWidget extends StatelessWidget {
final String text;
AttributionPluginWidget({this.text = ""}); const AttributionPluginWidget({super.key, this.text = ''});
final String text;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var style = new TextStyle( const TextStyle style = TextStyle(
fontSize: 12.0, fontSize: 12.0,
color: Colors.white, color: Colors.white,
); );
return Padding( return Padding(
padding: const EdgeInsets.all(5.0), padding: const EdgeInsets.all(5.0),
child: new Text( child: Text(
text, text,
style: style, style: style,
), ),

View file

@ -1,11 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
const fires50 = const Color(0xFFFBE9E7); const Color fires50 = Color(0xFFFBE9E7);
const fires100 = const Color(0xFFFFCCBC); const Color fires100 = Color(0xFFFFCCBC);
const fires300 = const Color(0xFFFF8A65); const Color fires300 = Color(0xFFFF8A65);
const fires600 = const Color(0xFFF4511E); const Color fires600 = Color(0xFFF4511E);
const fires900 = const Color(0xFFBF360C); const Color fires900 = Color(0xFFBF360C);
const firesErrorRed = const Color(0xFFDD2C00); const Color firesErrorRed = Color(0xFFDD2C00);
const firesSurfaceWhite = fires50; const Color firesSurfaceWhite = fires50;
const firesBackgroundWhite = Colors.white; const Color firesBackgroundWhite = Colors.white;

View file

@ -7,6 +7,8 @@ import 'package:flutter_map/flutter_map.dart';
/// Compass button widget for resetting map rotation to north /// Compass button widget for resetting map rotation to north
/// Only visible when map is rotated (rotation != 0) /// Only visible when map is rotated (rotation != 0)
class CompassMapPluginWidget extends StatefulWidget { class CompassMapPluginWidget extends StatefulWidget {
const CompassMapPluginWidget({super.key});
@override @override
_CompassMapPluginWidgetState createState() => _CompassMapPluginWidgetState(); _CompassMapPluginWidgetState createState() => _CompassMapPluginWidgetState();
} }
@ -29,7 +31,7 @@ class _CompassMapPluginWidgetState extends State<CompassMapPluginWidget> {
void _initRotationMonitoring() { void _initRotationMonitoring() {
// Check rotation periodically (every 100ms) to detect changes // Check rotation periodically (every 100ms) to detect changes
_rotationCheckTimer = Timer.periodic(Duration(milliseconds: 100), (_) { _rotationCheckTimer = Timer.periodic(const Duration(milliseconds: 100), (_) {
_checkRotation(); _checkRotation();
}); });
@ -41,8 +43,8 @@ class _CompassMapPluginWidgetState extends State<CompassMapPluginWidget> {
try { try {
if (!mounted) return; if (!mounted) return;
final rotation = _mapController.camera.rotation; final double rotation = _mapController.camera.rotation;
final isRotated = rotation.abs() > 0.0; final bool isRotated = rotation.abs() > 0.0;
if (isRotated != _isRotated) { if (isRotated != _isRotated) {
setState(() { setState(() {
@ -56,7 +58,7 @@ class _CompassMapPluginWidgetState extends State<CompassMapPluginWidget> {
void _resetRotation() { void _resetRotation() {
try { try {
final controller = MapController.of(context); final MapController controller = MapController.of(context);
controller.rotate(0); controller.rotate(0);
} catch (e) { } catch (e) {
print('Error resetting rotation: $e'); print('Error resetting rotation: $e');
@ -66,7 +68,7 @@ class _CompassMapPluginWidgetState extends State<CompassMapPluginWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) => Stack( builder: (BuildContext context, BoxConstraints constraints) => Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: <Widget>[ children: <Widget>[
if (_isRotated) if (_isRotated)
@ -94,7 +96,7 @@ class _CompassMapPluginWidgetState extends State<CompassMapPluginWidget> {
heroTag: 'compass_button', heroTag: 'compass_button',
tooltip: 'Go to North', tooltip: 'Go to North',
onPressed: _resetRotation, onPressed: _resetRotation,
child: Icon(Icons.explore), child: const Icon(Icons.explore),
); );
} }

View file

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
class CustomBottomAppBar extends StatelessWidget { class CustomBottomAppBar extends StatelessWidget {
const CustomBottomAppBar( const CustomBottomAppBar(
{this.fabLocation, {super.key, this.fabLocation,
this.showNotch = false, this.showNotch = false,
this.height = 56.0, this.height = 56.0,
this.color = Colors.black45, this.color = Colors.black45,
@ -24,7 +24,7 @@ class CustomBottomAppBar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<Widget> rowContents = <Widget>[new SizedBox(height: height)]; final List<Widget> rowContents = <Widget>[SizedBox(height: height)];
if (kCenterLocations.contains(fabLocation)) { if (kCenterLocations.contains(fabLocation)) {
/* rowContents.add( /* rowContents.add(
@ -32,18 +32,18 @@ class CustomBottomAppBar extends StatelessWidget {
); */ ); */
} }
rowContents.addAll(this.actions); rowContents.addAll(actions);
return showNotch return showNotch
? new BottomAppBar( ? BottomAppBar(
color: color, color: color,
shape: CircularNotchedRectangle(), shape: const CircularNotchedRectangle(),
child: new Row( child: Row(
mainAxisAlignment: mainAxisAlignment, children: rowContents), mainAxisAlignment: mainAxisAlignment, children: rowContents),
) )
: new BottomAppBar( : BottomAppBar(
color: color, color: color,
child: new Row( child: Row(
mainAxisAlignment: mainAxisAlignment, children: rowContents), mainAxisAlignment: mainAxisAlignment, children: rowContents),
); );
} }

View file

@ -1,14 +1,14 @@
// Copyright (c) 2016, rinukkusu. All rights reserved. Use of this source code // Copyright (c) 2016, rinukkusu. All rights reserved. Use of this source code
// is governed by a BSD-style license that can be found in the LICENSE file. // is governed by a BSD-style license that can be found in the LICENSE file.
import 'generated/i18n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'generated/i18n.dart';
class Moment { class Moment {
late DateTime _date;
Moment.now() { Moment.now() {
_date = new DateTime.now(); _date = DateTime.now();
} }
Moment.fromDate(DateTime date) { Moment.fromDate(DateTime date) {
@ -17,27 +17,29 @@ class Moment {
Moment.fromMillisecondsSinceEpoch(int millisecondsSinceEpoch, Moment.fromMillisecondsSinceEpoch(int millisecondsSinceEpoch,
{bool isUtc = false}) { {bool isUtc = false}) {
_date = new DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch, _date = DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch,
isUtc: isUtc); isUtc: isUtc);
} }
late DateTime _date;
static Moment parse(String date) { static Moment parse(String date) {
return new Moment.fromDate(DateTime.parse(date)); return Moment.fromDate(DateTime.parse(date));
} }
@override
String toString() { String toString() {
return _date.toString(); return _date.toString();
} }
String fromNow(BuildContext context, [bool withoutPrefixOrSuffix = false]) { String fromNow(BuildContext context, [bool withoutPrefixOrSuffix = false]) {
return from(context, new DateTime.now(), withoutPrefixOrSuffix); return from(context, DateTime.now(), withoutPrefixOrSuffix);
} }
String from(BuildContext context, DateTime date, String from(BuildContext context, DateTime date,
[bool withoutPrefixOrSuffix = false]) { [bool withoutPrefixOrSuffix = false]) {
Duration diff = date.difference(_date); final Duration diff = date.difference(_date);
String timeString = ""; String timeString = '';
if (diff.inSeconds.abs() < 45) if (diff.inSeconds.abs() < 45)
timeString = S.of(context).aF3wSeconds; timeString = S.of(context).aF3wSeconds;

View file

@ -41,7 +41,7 @@ enum CustomStepperType {
horizontal, horizontal,
} }
const TextStyle _kCustomStepStyle = const TextStyle( const TextStyle _kCustomStepStyle = TextStyle(
fontSize: 12.0, fontSize: 12.0,
color: Colors.white, color: Colors.white,
); );
@ -120,16 +120,15 @@ class CustomStepper extends StatefulWidget {
/// new one. /// new one.
/// ///
/// The [steps], [type], and [currentCustomStep] arguments must not be null. /// The [steps], [type], and [currentCustomStep] arguments must not be null.
CustomStepper({ const CustomStepper({
Key? key, super.key,
required this.steps, required this.steps,
this.type = CustomStepperType.vertical, this.type = CustomStepperType.vertical,
this.currentCustomStep = 0, this.currentCustomStep = 0,
this.onCustomStepTapped, this.onCustomStepTapped,
this.onCustomStepContinue, this.onCustomStepContinue,
this.onCustomStepCancel, this.onCustomStepCancel,
}) : assert(0 <= currentCustomStep && currentCustomStep < steps.length), }) : assert(0 <= currentCustomStep && currentCustomStep < steps.length);
super(key: key);
/// The steps of the stepper whose titles, subtitles, icons always get shown. /// The steps of the stepper whose titles, subtitles, icons always get shown.
/// ///
@ -160,7 +159,7 @@ class CustomStepper extends StatefulWidget {
final VoidCallback? onCustomStepCancel; final VoidCallback? onCustomStepCancel;
@override @override
_CustomStepperState createState() => new _CustomStepperState(); _CustomStepperState createState() => _CustomStepperState();
} }
class _CustomStepperState extends State<CustomStepper> { class _CustomStepperState extends State<CustomStepper> {
@ -170,9 +169,9 @@ class _CustomStepperState extends State<CustomStepper> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_keys = new List<GlobalKey>.generate( _keys = List<GlobalKey>.generate(
widget.steps.length, widget.steps.length,
(int i) => new GlobalKey(), (int i) => GlobalKey(),
); );
for (int i = 0; i < widget.steps.length; i += 1) for (int i = 0; i < widget.steps.length; i += 1)
@ -205,7 +204,7 @@ class _CustomStepperState extends State<CustomStepper> {
} }
Widget _buildLine(bool visible) { Widget _buildLine(bool visible) {
return new Container( return Container(
width: visible ? 1.0 : 0.0, width: visible ? 1.0 : 0.0,
height: 16.0, height: 16.0,
color: Colors.grey.shade400, color: Colors.grey.shade400,
@ -219,19 +218,19 @@ class _CustomStepperState extends State<CustomStepper> {
switch (state) { switch (state) {
case CustomStepState.indexed: case CustomStepState.indexed:
case CustomStepState.disabled: case CustomStepState.disabled:
return new Text( return Text(
'${index + 1}', '${index + 1}',
style: isDarkActive style: isDarkActive
? _kCustomStepStyle.copyWith(color: Colors.black87) ? _kCustomStepStyle.copyWith(color: Colors.black87)
: _kCustomStepStyle, : _kCustomStepStyle,
); );
case CustomStepState.editing: case CustomStepState.editing:
return new Icon( return Icon(
Icons.edit, Icons.edit,
color: isDarkActive ? _kCircleActiveDark : _kCircleActiveLight, color: isDarkActive ? _kCircleActiveDark : _kCircleActiveLight,
); );
case CustomStepState.complete: case CustomStepState.complete:
return new Icon( return Icon(
Icons.check, Icons.check,
color: isDarkActive ? _kCircleActiveDark : _kCircleActiveLight, color: isDarkActive ? _kCircleActiveDark : _kCircleActiveLight,
); );
@ -255,18 +254,18 @@ class _CustomStepperState extends State<CustomStepper> {
} }
Widget _buildCircle(int index, bool oldState) { Widget _buildCircle(int index, bool oldState) {
return new Container( return Container(
margin: const EdgeInsets.symmetric(vertical: 8.0), margin: const EdgeInsets.symmetric(vertical: 8.0),
width: _kCustomStepSize, width: _kCustomStepSize,
height: _kCustomStepSize, height: _kCustomStepSize,
child: new AnimatedContainer( child: AnimatedContainer(
curve: Curves.fastOutSlowIn, curve: Curves.fastOutSlowIn,
duration: kThemeAnimationDuration, duration: kThemeAnimationDuration,
decoration: new BoxDecoration( decoration: BoxDecoration(
color: _circleColor(index), color: _circleColor(index),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: new Center( child: Center(
child: _buildCircleChild(index, child: _buildCircleChild(index,
oldState && widget.steps[index].state == CustomStepState.error), oldState && widget.steps[index].state == CustomStepState.error),
), ),
@ -275,20 +274,20 @@ class _CustomStepperState extends State<CustomStepper> {
} }
Widget _buildTriangle(int index, bool oldState) { Widget _buildTriangle(int index, bool oldState) {
return new Container( return Container(
margin: const EdgeInsets.symmetric(vertical: 8.0), margin: const EdgeInsets.symmetric(vertical: 8.0),
width: _kCustomStepSize, width: _kCustomStepSize,
height: _kCustomStepSize, height: _kCustomStepSize,
child: new Center( child: Center(
child: new SizedBox( child: SizedBox(
width: _kCustomStepSize, width: _kCustomStepSize,
height: height:
_kTriangleHeight, // Height of 24dp-long-sided equilateral triangle. _kTriangleHeight, // Height of 24dp-long-sided equilateral triangle.
child: new CustomPaint( child: CustomPaint(
painter: new _TrianglePainter( painter: _TrianglePainter(
color: _isDark() ? _kErrorDark : _kErrorLight, color: _isDark() ? _kErrorDark : _kErrorLight,
), ),
child: new Align( child: Align(
alignment: const Alignment( alignment: const Alignment(
0.0, 0.8), // 0.8 looks better than the geometrical 0.33. 0.0, 0.8), // 0.8 looks better than the geometrical 0.33.
child: _buildCircleChild( child: _buildCircleChild(
@ -304,7 +303,7 @@ class _CustomStepperState extends State<CustomStepper> {
Widget _buildIcon(int index) { Widget _buildIcon(int index) {
if (widget.steps[index].state != _oldStates[index]) { if (widget.steps[index].state != _oldStates[index]) {
return new AnimatedCrossFade( return AnimatedCrossFade(
firstChild: _buildCircle(index, true), firstChild: _buildCircle(index, true),
secondChild: _buildTriangle(index, true), secondChild: _buildTriangle(index, true),
firstCurve: const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn), firstCurve: const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),
@ -339,29 +338,12 @@ class _CustomStepperState extends State<CustomStepper> {
// final MaterialLocalizations localizations = // final MaterialLocalizations localizations =
// MaterialLocalizations.of(context); // MaterialLocalizations.of(context);
return new Container( return Container(
margin: const EdgeInsets.only(top: 16.0), margin: const EdgeInsets.only(top: 16.0),
child: new ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints.tightFor(height: 48.0), constraints: const BoxConstraints.tightFor(height: 48.0),
child: new Row( child: const Row(
children: <Widget>[
/* new FlatButton(
onPressed: widget.onCustomStepContinue,
color: _isDark() ? themeData.backgroundColor : themeData.primaryColor,
textColor: Colors.white,
textTheme: ButtonTextTheme.normal,
child: new Text(localizations.continueButtonLabel),
),
new Container(
margin: const EdgeInsetsDirectional.only(start: 8.0),
child: new FlatButton(
onPressed: widget.onCustomStepCancel,
textColor: cancelColor,
textTheme: ButtonTextTheme.normal,
child: new Text(localizations.cancelButtonLabel),
),
), */
],
), ),
), ),
); );
@ -407,7 +389,7 @@ class _CustomStepperState extends State<CustomStepper> {
Widget _buildHeaderText(int index) { Widget _buildHeaderText(int index) {
final List<Widget> children = <Widget>[ final List<Widget> children = <Widget>[
new AnimatedDefaultTextStyle( AnimatedDefaultTextStyle(
style: _titleStyle(index), style: _titleStyle(index),
duration: kThemeAnimationDuration, duration: kThemeAnimationDuration,
curve: Curves.fastOutSlowIn, curve: Curves.fastOutSlowIn,
@ -417,9 +399,9 @@ class _CustomStepperState extends State<CustomStepper> {
if (widget.steps[index].subtitle != null) { if (widget.steps[index].subtitle != null) {
children.add( children.add(
new Container( Container(
margin: const EdgeInsets.only(top: 2.0), margin: const EdgeInsets.only(top: 2.0),
child: new AnimatedDefaultTextStyle( child: AnimatedDefaultTextStyle(
style: _subtitleStyle(index), style: _subtitleStyle(index),
duration: kThemeAnimationDuration, duration: kThemeAnimationDuration,
curve: Curves.fastOutSlowIn, curve: Curves.fastOutSlowIn,
@ -429,57 +411,57 @@ class _CustomStepperState extends State<CustomStepper> {
); );
} }
return new Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: children); children: children);
} }
Widget _buildVerticalHeader(int index) { Widget _buildVerticalHeader(int index) {
return new Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 24.0), margin: const EdgeInsets.symmetric(horizontal: 24.0),
child: new Row(children: <Widget>[ child: Row(children: <Widget>[
new Column(children: <Widget>[ Column(children: <Widget>[
// Line parts are always added in order for the ink splash to // Line parts are always added in order for the ink splash to
// flood the tips of the connector lines. // flood the tips of the connector lines.
_buildLine(!_isFirst(index)), _buildLine(!_isFirst(index)),
_buildIcon(index), _buildIcon(index),
_buildLine(!_isLast(index)), _buildLine(!_isLast(index)),
]), ]),
new Container( Container(
margin: const EdgeInsetsDirectional.only(start: 12.0), margin: const EdgeInsetsDirectional.only(start: 12.0),
child: _buildHeaderText(index)) child: _buildHeaderText(index))
])); ]));
} }
Widget _buildVerticalBody(int index) { Widget _buildVerticalBody(int index) {
return new Stack( return Stack(
children: <Widget>[ children: <Widget>[
new PositionedDirectional( PositionedDirectional(
start: 24.0, start: 24.0,
top: 0.0, top: 0.0,
bottom: 0.0, bottom: 0.0,
child: new SizedBox( child: SizedBox(
width: 24.0, width: 24.0,
child: new Center( child: Center(
child: new SizedBox( child: SizedBox(
width: _isLast(index) ? 0.0 : 1.0, width: _isLast(index) ? 0.0 : 1.0,
child: new Container( child: Container(
color: Colors.grey.shade400, color: Colors.grey.shade400,
), ),
), ),
), ),
), ),
), ),
new AnimatedCrossFade( AnimatedCrossFade(
firstChild: new Container(height: 0.0), firstChild: Container(height: 0.0),
secondChild: new Container( secondChild: Container(
margin: const EdgeInsetsDirectional.only( margin: const EdgeInsetsDirectional.only(
start: 60.0, start: 60.0,
end: 24.0, end: 24.0,
bottom: 24.0, bottom: 24.0,
), ),
child: new Column( child: Column(
children: <Widget>[ children: <Widget>[
widget.steps[index].content, widget.steps[index].content,
_buildVerticalControls(), _buildVerticalControls(),
@ -502,8 +484,8 @@ class _CustomStepperState extends State<CustomStepper> {
final List<Widget> children = <Widget>[]; final List<Widget> children = <Widget>[];
for (int i = 0; i < widget.steps.length; i += 1) { for (int i = 0; i < widget.steps.length; i += 1) {
children.add(new Column(key: _keys[i], children: <Widget>[ children.add(Column(key: _keys[i], children: <Widget>[
new InkWell( InkWell(
onTap: widget.steps[i].state != CustomStepState.disabled onTap: widget.steps[i].state != CustomStepState.disabled
? () { ? () {
// In the vertical case we need to scroll to the newly tapped // In the vertical case we need to scroll to the newly tapped
@ -522,7 +504,7 @@ class _CustomStepperState extends State<CustomStepper> {
])); ]));
} }
return new ListView( return ListView(
shrinkWrap: true, shrinkWrap: true,
children: children, children: children,
); );
@ -533,21 +515,21 @@ class _CustomStepperState extends State<CustomStepper> {
for (int i = 0; i < widget.steps.length; i += 1) { for (int i = 0; i < widget.steps.length; i += 1) {
children.add( children.add(
new InkResponse( InkResponse(
onTap: widget.steps[i].state != CustomStepState.disabled onTap: widget.steps[i].state != CustomStepState.disabled
? () { ? () {
widget.onCustomStepTapped?.call(i); widget.onCustomStepTapped?.call(i);
} }
: null, : null,
child: new Row( child: Row(
children: <Widget>[ children: <Widget>[
new Container( SizedBox(
height: 72.0, height: 72.0,
child: new Center( child: Center(
child: _buildIcon(i), child: _buildIcon(i),
), ),
), ),
new Container( Container(
margin: const EdgeInsetsDirectional.only(start: 12.0), margin: const EdgeInsetsDirectional.only(start: 12.0),
child: _buildHeaderText(i), child: _buildHeaderText(i),
), ),
@ -558,8 +540,8 @@ class _CustomStepperState extends State<CustomStepper> {
if (!_isLast(i)) { if (!_isLast(i)) {
children.add( children.add(
new Expanded( Expanded(
child: new Container( child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0), margin: const EdgeInsets.symmetric(horizontal: 8.0),
height: 1.0, height: 1.0,
color: Colors.grey.shade400, color: Colors.grey.shade400,
@ -569,22 +551,22 @@ class _CustomStepperState extends State<CustomStepper> {
} }
} }
return new Column( return Column(
children: <Widget>[ children: <Widget>[
new Material( Material(
elevation: 2.0, elevation: 2.0,
child: new Container( child: Container(
margin: const EdgeInsets.symmetric(horizontal: 24.0), margin: const EdgeInsets.symmetric(horizontal: 24.0),
child: new Row( child: Row(
children: children, children: children,
), ),
), ),
), ),
new Expanded( Expanded(
child: new ListView( child: ListView(
padding: const EdgeInsets.all(24.0), padding: const EdgeInsets.all(24.0),
children: <Widget>[ children: <Widget>[
new AnimatedSize( AnimatedSize(
curve: Curves.fastOutSlowIn, curve: Curves.fastOutSlowIn,
duration: kThemeAnimationDuration, duration: kThemeAnimationDuration,
child: widget.steps[widget.currentCustomStep].content, child: widget.steps[widget.currentCustomStep].content,
@ -602,7 +584,7 @@ class _CustomStepperState extends State<CustomStepper> {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
assert(() { assert(() {
if (context.findAncestorWidgetOfExactType<CustomStepper>() != null) if (context.findAncestorWidgetOfExactType<CustomStepper>() != null)
throw new FlutterError( throw FlutterError(
'CustomSteppers must not be nested. The material specification advises ' 'CustomSteppers must not be nested. The material specification advises '
'that one should avoid embedding steppers within steppers. ' 'that one should avoid embedding steppers within steppers. '
'https://material.google.com/components/steppers.html#steppers-usage\n'); 'https://material.google.com/components/steppers.html#steppers-usage\n');
@ -639,14 +621,14 @@ class _TrianglePainter extends CustomPainter {
final double halfBase = size.width / 2.0; final double halfBase = size.width / 2.0;
final double height = size.height; final double height = size.height;
final List<Offset> points = <Offset>[ final List<Offset> points = <Offset>[
new Offset(0.0, height), Offset(0.0, height),
new Offset(base, height), Offset(base, height),
new Offset(halfBase, 0.0), Offset(halfBase, 0.0),
]; ];
canvas.drawPath( canvas.drawPath(
new Path()..addPolygon(points, true), Path()..addPolygon(points, true),
new Paint()..color = color, Paint()..color = color,
); );
} }
} }

View file

@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
/// Dummy/placeholder widget for map plugins /// Dummy/placeholder widget for map plugins
class DummyMapPluginWidget extends StatelessWidget { class DummyMapPluginWidget extends StatelessWidget {
const DummyMapPluginWidget({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const SizedBox(); return const SizedBox();

View file

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:flutter/services.dart' show rootBundle; import 'package:flutter/services.dart' show rootBundle;
final esRegExp = new RegExp("^es-"); final RegExp esRegExp = RegExp('^es-');
String getFallbackLang(String lang) { String getFallbackLang(String lang) {
if (lang == 'ast' || if (lang == 'ast' ||
@ -10,7 +10,7 @@ String getFallbackLang(String lang) {
lang == 'eu' || lang == 'eu' ||
lang == 'es' || lang == 'es' ||
lang == 'ca' || lang == 'ca' ||
(esRegExp).allMatches(lang).length > 0) { esRegExp.allMatches(lang).isNotEmpty) {
return 'es'; return 'es';
} }
return 'en'; return 'en';
@ -21,8 +21,8 @@ Future<String> getFileNameOfLang(
required String fileName, required String fileName,
required String ext, required String ext,
required String lang}) async { required String lang}) async {
String base = '$dir/$fileName'; final String base = '$dir/$fileName';
String fallback = getFallbackLang(lang); final String fallback = getFallbackLang(lang);
String file = '$base-$lang.$ext'; String file = '$base-$lang.$ext';
if (await assetNotExists(file)) { if (await assetNotExists(file)) {
file = '$base-$fallback.$ext'; file = '$base-$fallback.$ext';

View file

@ -7,9 +7,12 @@ import 'package:url_launcher/url_launcher.dart';
import 'customStepper.dart'; import 'customStepper.dart';
import 'generated/i18n.dart'; import 'generated/i18n.dart';
import 'mainDrawer.dart'; import 'mainDrawer.dart';
import 'models/yourLocation.dart';
import 'placesAutocompleteUtils.dart'; import 'placesAutocompleteUtils.dart';
class FireAlert extends StatefulWidget { class FireAlert extends StatefulWidget {
const FireAlert({super.key});
static const String routeName = '/fireAlert'; static const String routeName = '/fireAlert';
@override @override
@ -17,31 +20,31 @@ class FireAlert extends StatefulWidget {
} }
class _FireAlertState extends State<FireAlert> { class _FireAlertState extends State<FireAlert> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
int _currentStep = 0; int _currentStep = 0;
Widget buildCallButton() { Widget buildCallButton() {
return new Align( return Align(
alignment: const Alignment(0.0, -0.2), alignment: const Alignment(0.0, -0.2),
child: new FloatingActionButton( child: FloatingActionButton(
heroTag: 'callAction', heroTag: 'callAction',
child: const Icon(Icons.call), child: const Icon(Icons.call),
onPressed: () { onPressed: () {
launch("tel://112"); launch('tel://112');
}, },
), ),
); );
} }
Widget buildNotifyNeighboursButton() { Widget buildNotifyNeighboursButton() {
return new Align( return Align(
alignment: const Alignment(0.0, -0.2), alignment: const Alignment(0.0, -0.2),
child: new FloatingActionButton( child: FloatingActionButton(
heroTag: 'neighAction', heroTag: 'neighAction',
child: const Icon(CommunityMaterialIcons.bullhorn), child: const Icon(CommunityMaterialIcons.bullhorn),
onPressed: () { onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(new SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: new Text(S.of(context).inDevelopment), content: Text(S.of(context).inDevelopment),
)); ));
}, },
), ),
@ -49,26 +52,26 @@ class _FireAlertState extends State<FireAlert> {
} }
Widget buildTweetButton() { Widget buildTweetButton() {
return new Align( return Align(
alignment: const Alignment(0.0, -0.2), alignment: const Alignment(0.0, -0.2),
child: new FloatingActionButton( child: FloatingActionButton(
heroTag: 'tweetAction', heroTag: 'tweetAction',
child: const Icon(CommunityMaterialIcons.twitter), child: const Icon(CommunityMaterialIcons.twitter),
onPressed: () { onPressed: () {
// In Android you can choose with app to use with setPackage but seems it's not implemented here // In Android you can choose with app to use with setPackage but seems it's not implemented here
// https://stackoverflow.com/questions/6814268/android-share-on-facebook-twitter-mail-ecc // https://stackoverflow.com/questions/6814268/android-share-on-facebook-twitter-mail-ecc
openPlacesDialog(_scaffoldKey).then((yourLocation) { openPlacesDialog(_scaffoldKey).then((YourLocation yourLocation) {
String where = final String where =
yourLocation.description.replaceAll(' ', '').split(',')[0]; yourLocation.description.replaceAll(' ', '').split(',')[0];
print(where); print(where);
Share.shareWithResult(S Share.shareWithResult(S
.of(context) .of(context)
.tweetAboutSelf(yourLocation.description, '#IF$where')); .tweetAboutSelf(yourLocation.description, '#IF$where'));
}).catchError((onError) { }).catchError((onError) {
final ctx = _scaffoldKey.currentContext; final BuildContext? ctx = _scaffoldKey.currentContext;
if (ctx != null) { if (ctx != null) {
ScaffoldMessenger.of(ctx).showSnackBar(new SnackBar( ScaffoldMessenger.of(ctx).showSnackBar(SnackBar(
content: new Text(S.of(ctx).errorFirePlaceDialog))); content: Text(S.of(ctx).errorFirePlaceDialog)));
} }
}); });
}, },
@ -81,40 +84,40 @@ class _FireAlertState extends State<FireAlert> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<CustomStep> fireSteps = listWithoutNulls(<CustomStep>[ final List<CustomStep> fireSteps = listWithoutNulls(<CustomStep>[
new CustomStep( CustomStep(
title: new Text(S.of(context).callEmergencyServicesTitle), title: Text(S.of(context).callEmergencyServicesTitle),
content: new Column(children: <Widget>[ content: Column(children: <Widget>[
new Text(S.of(context).callEmergencyServicesDescription), Text(S.of(context).callEmergencyServicesDescription),
new SizedBox(height: 20.0), const SizedBox(height: 20.0),
buildCallButton() buildCallButton()
])), ])),
new CustomStep( CustomStep(
title: new Text(S.of(context).notifyNeighbours), title: Text(S.of(context).notifyNeighbours),
// state: CustomStepState.disabled, // state: CustomStepState.disabled,
content: new Column(children: <Widget>[ content: Column(children: <Widget>[
new Text(S.of(context).notifyNeighboursDescription), Text(S.of(context).notifyNeighboursDescription),
new SizedBox(height: 20.0), const SizedBox(height: 20.0),
buildNotifyNeighboursButton() buildNotifyNeighboursButton()
])), ])),
// TODO conditional: this only in Spain // TODOconditional: this only in Spain
new CustomStep( CustomStep(
title: new Text(S.of(context).tweetAboutAFireTitle), title: Text(S.of(context).tweetAboutAFireTitle),
// subtitle: new Text(S.of(context).tweetAboutAFireDescription), // subtitle: new Text(S.of(context).tweetAboutAFireDescription),
content: new Column(children: <Widget>[ content: Column(children: <Widget>[
new Text(S.of(context).tweetAboutAFireDescription), Text(S.of(context).tweetAboutAFireDescription),
new SizedBox(height: 20.0), const SizedBox(height: 20.0),
buildTweetButton() buildTweetButton()
])), ])),
]); ]);
return Scaffold( return Scaffold(
key: _scaffoldKey, key: _scaffoldKey,
appBar: new AppBar(title: new Text(S.of(context).notifyAFire)), appBar: AppBar(title: Text(S.of(context).notifyAFire)),
drawer: new MainDrawer(context, FireAlert.routeName), drawer: MainDrawer(context, FireAlert.routeName),
body: new CustomStepper( body: CustomStepper(
currentCustomStep: _currentStep, currentCustomStep: _currentStep,
// type: StepperType.horizontal, // type: StepperType.horizontal,
onCustomStepTapped: (num) => setState(() { onCustomStepTapped: (int num) => setState(() {
_currentStep = num; _currentStep = num;
}), }),
steps: fireSteps)); steps: fireSteps));

View file

@ -31,16 +31,16 @@ Marker FireMarker(
Offset _getAnchorOffset(FireMarkType type) { Offset _getAnchorOffset(FireMarkType type) {
switch (type) { switch (type) {
case FireMarkType.position: case FireMarkType.position:
return Offset(40.0, 20.0); return const Offset(40.0, 20.0);
case FireMarkType.fire: case FireMarkType.fire:
return Offset(40.0, 24.0); return const Offset(40.0, 24.0);
case FireMarkType.pixel: case FireMarkType.pixel:
// Auto-calculate based on marker size // Auto-calculate based on marker size
return Offset(40.0, 40.0); return const Offset(40.0, 40.0);
case FireMarkType.falsePos: case FireMarkType.falsePos:
case FireMarkType.industry: case FireMarkType.industry:
return Offset(40.0, 35.0); return const Offset(40.0, 35.0);
default: default:
return Offset(40.0, 40.0); return const Offset(40.0, 40.0);
} }
} }

View file

@ -1,26 +1,27 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'fireMarkType.dart';
import 'colors.dart'; import 'colors.dart';
import 'fireMarkType.dart';
class FireMarkerIcon extends StatelessWidget { class FireMarkerIcon extends StatelessWidget {
final FireMarkType type;
FireMarkerIcon(this.type); const FireMarkerIcon(this.type, {super.key});
final FireMarkType type;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
switch (type) { switch (type) {
case FireMarkType.position: case FireMarkType.position:
return new Icon(Icons.location_on, color: fires600, size: 50.0); return const Icon(Icons.location_on, color: fires600, size: 50.0);
case FireMarkType.pixel: case FireMarkType.pixel:
return new Icon(Icons.brightness_1, color: fires900, size: 3.0); return const Icon(Icons.brightness_1, color: fires900, size: 3.0);
case FireMarkType.fire: case FireMarkType.fire:
return new Image.asset('images/fire-marker-l.png'); return Image.asset('images/fire-marker-l.png');
case FireMarkType.industry: case FireMarkType.industry:
return new Image.asset('images/industry-marker-reg.png'); return Image.asset('images/industry-marker-reg.png');
case FireMarkType.falsePos: case FireMarkType.falsePos:
default: default:
return new Image.asset('images/industry-marker.png'); return Image.asset('images/industry-marker.png');
} }
} }
} }

View file

@ -1,8 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:fires_flutter/models/fireNotification.dart';
import 'package:fires_flutter/models/yourLocation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart'; import 'package:redux/redux.dart';
@ -13,19 +11,14 @@ import 'generated/i18n.dart';
import 'genericMap.dart'; import 'genericMap.dart';
import 'mainDrawer.dart'; import 'mainDrawer.dart';
import 'models/appState.dart'; import 'models/appState.dart';
import 'models/fireNotification.dart';
import 'models/yourLocation.dart';
import 'redux/actions.dart'; import 'redux/actions.dart';
@immutable @immutable
class _ViewModel { class _ViewModel {
final bool isLoaded;
final List<FireNotification> fireNotifications;
final int fireNotificationsUnread;
final List<YourLocation> yourLocations;
final TapFireNotificationFunction onTap;
final DeleteFireNotificationFunction onDelete;
final DeleteAllFireNotificationFunction onDeleteAll;
_ViewModel( const _ViewModel(
{required this.isLoaded, {required this.isLoaded,
required this.onTap, required this.onTap,
required this.onDelete, required this.onDelete,
@ -33,6 +26,13 @@ class _ViewModel {
required this.fireNotifications, required this.fireNotifications,
required this.yourLocations, required this.yourLocations,
required this.fireNotificationsUnread}); required this.fireNotificationsUnread});
final bool isLoaded;
final List<FireNotification> fireNotifications;
final int fireNotificationsUnread;
final List<YourLocation> yourLocations;
final TapFireNotificationFunction onTap;
final DeleteFireNotificationFunction onDelete;
final DeleteAllFireNotificationFunction onDeleteAll;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@ -53,6 +53,8 @@ class _ViewModel {
} }
class FireNotificationList extends StatefulWidget { class FireNotificationList extends StatefulWidget {
const FireNotificationList({super.key});
static const String routeName = '/fireNotifications'; static const String routeName = '/fireNotifications';
@override @override
@ -60,23 +62,23 @@ class FireNotificationList extends StatefulWidget {
} }
class _FireNotificationListState extends State<FireNotificationList> { class _FireNotificationListState extends State<FireNotificationList> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
Widget _buildRow(BuildContext context, List<YourLocation> yourLocations, Widget _buildRow(BuildContext context, List<YourLocation> yourLocations,
FireNotification notif, onDeleted, onTap) { FireNotification notif, onDeleted, onTap) {
String prefix = ""; String prefix = '';
// FIXME (this can fails if you don't have a location for this notif, for instance during tests) // FIXME (this can fails if you don't have a location for this notif, for instance during tests)
YourLocation yl = yourLocations.singleWhere((yl) => yl.id == notif.subsId); final YourLocation yl = yourLocations.singleWhere((YourLocation yl) => yl.id == notif.subsId);
prefix = '${yl.description}. '; prefix = '${yl.description}. ';
return new ListTile( return ListTile(
dense: true, dense: true,
leading: const Icon(Icons.whatshot), leading: const Icon(Icons.whatshot),
title: new Text('$prefix${notif.description}', title: Text('$prefix${notif.description}',
style: new TextStyle( style: TextStyle(
fontWeight: notif.read ? FontWeight.normal : FontWeight.bold)), fontWeight: notif.read ? FontWeight.normal : FontWeight.bold)),
subtitle: new Text(Moment.now().from(context, notif.when)), subtitle: Text(Moment.now().from(context, notif.when)),
onLongPress: () { onLongPress: () {
showSnackMsg(S.of(context).toDeleteThisNotification); showSnackMsg(S.of(context).toDeleteThisNotification);
}, },
@ -86,8 +88,8 @@ class _FireNotificationListState extends State<FireNotificationList> {
} }
void showSnackMsg(String msg) { void showSnackMsg(String msg) {
ScaffoldMessenger.of(context).showSnackBar(new SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: new Text(msg), content: Text(msg),
)); ));
} }
@ -97,78 +99,77 @@ class _FireNotificationListState extends State<FireNotificationList> {
List<FireNotification> notifList, List<FireNotification> notifList,
onDeleted, onDeleted,
onTap) { onTap) {
return new RefreshIndicator( return RefreshIndicator(
child: new ListView.builder( onRefresh: _handleRefresh,
child: ListView.builder(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
// reverse: true, // reverse: true,
// shrinkWrap: true, // shrinkWrap: true,
itemCount: notifList.length, itemCount: notifList.length,
itemBuilder: (BuildContext _context, int i) { itemBuilder: (BuildContext context, int i) {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
return new Dismissible( return Dismissible(
key: new ObjectKey(notifList.elementAt(i)), key: ObjectKey(notifList.elementAt(i)),
direction: DismissDirection.horizontal,
onDismissed: (DismissDirection direction) { onDismissed: (DismissDirection direction) {
onDeleted(notifList.elementAt(i)); onDeleted(notifList.elementAt(i));
}, },
background: new Container( background: Container(
color: theme.primaryColor, color: theme.primaryColor,
child: const ListTile( child: const ListTile(
leading: const Icon(Icons.delete, leading: Icon(Icons.delete,
color: Colors.white, size: 36.0))), color: Colors.white, size: 36.0))),
secondaryBackground: new Container( secondaryBackground: Container(
color: theme.primaryColor, color: theme.primaryColor,
child: const ListTile( child: const ListTile(
trailing: const Icon(Icons.delete, trailing: Icon(Icons.delete,
color: Colors.white, size: 36.0))), color: Colors.white, size: 36.0))),
child: new Container( child: Container(
decoration: new BoxDecoration( decoration: BoxDecoration(
color: theme.canvasColor, color: theme.canvasColor,
border: new Border( border: Border(
bottom: bottom:
new BorderSide(color: theme.dividerColor))), BorderSide(color: theme.dividerColor))),
child: _buildRow(context, yourLocations, child: _buildRow(context, yourLocations,
notifList.elementAt(i), onDeleted, onTap))); notifList.elementAt(i), onDeleted, onTap)));
}), }));
onRefresh: _handleRefresh);
} }
Future<Null> _handleRefresh() async { Future<void> _handleRefresh() async {
await new Future.delayed(new Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
setState(() {}); setState(() {});
return null; return;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new StoreConnector<AppState, _ViewModel>( return StoreConnector<AppState, _ViewModel>(
distinct: true, distinct: true,
converter: (store) { converter: (Store<AppState> store) {
print( print(
'New ViewModel of Fires Notifications (unread: ${store.state.fireNotificationsUnread})'); 'New ViewModel of Fires Notifications (unread: ${store.state.fireNotificationsUnread})');
return new _ViewModel( return _ViewModel(
isLoaded: store.state.isLoaded, isLoaded: store.state.isLoaded,
onDeleteAll: () { onDeleteAll: () {
store.dispatch(new DeleteAllFireNotificationAction()); store.dispatch(DeleteAllFireNotificationAction());
}, },
onDelete: (notif) { onDelete: (FireNotification notif) {
store.dispatch(new DeleteFireNotificationAction(notif)); store.dispatch(DeleteFireNotificationAction(notif));
ScaffoldMessenger.of(context).showSnackBar(new SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: new Text(S.of(context).youDeletedThisNotification), content: Text(S.of(context).youDeletedThisNotification),
action: new SnackBarAction( action: SnackBarAction(
label: S.of(context).UNDO, label: S.of(context).UNDO,
onPressed: () { onPressed: () {
store.dispatch(new AddFireNotificationAction(notif)); store.dispatch(AddFireNotificationAction(notif));
}))); })));
}, },
onTap: (notif) { onTap: (FireNotification notif) {
if (!notif.read) { if (!notif.read) {
store.dispatch(new ReadFireNotificationAction( store.dispatch(ReadFireNotificationAction(
notif.copyWith(read: true))); notif.copyWith(read: true)));
} }
new Timer(new Duration(milliseconds: 500), () { Timer(const Duration(milliseconds: 500), () {
gotoMap(store, notif, context); gotoMap(store, notif, context);
}); });
}, },
@ -176,48 +177,46 @@ class _FireNotificationListState extends State<FireNotificationList> {
fireNotifications: store.state.fireNotifications, fireNotifications: store.state.fireNotifications,
fireNotificationsUnread: store.state.fireNotificationsUnread); fireNotificationsUnread: store.state.fireNotificationsUnread);
}, },
builder: (context, view) { builder: (BuildContext context, _ViewModel view) {
var hasFireNotifications = view.fireNotifications.length > 0; final bool hasFireNotifications = view.fireNotifications.isNotEmpty;
final title = S.of(context).fireNotificationsTitle; final String title = S.of(context).fireNotificationsTitle;
print('Building Fire Notifications List'); print('Building Fire Notifications List');
return Scaffold( return Scaffold(
key: _scaffoldKey, key: _scaffoldKey,
drawer: new MainDrawer(context, FireNotificationList.routeName), drawer: MainDrawer(context, FireNotificationList.routeName),
appBar: new AppBar( appBar: AppBar(
title: Text(title), title: Text(title),
leading: IconButton( leading: IconButton(
icon: Icon(Icons.menu), icon: const Icon(Icons.menu),
onPressed: () { onPressed: () {
_scaffoldKey.currentState?.openDrawer(); _scaffoldKey.currentState?.openDrawer();
}, },
), ),
actions: (<Widget?>[ actions: <Widget?>[
hasFireNotifications if (hasFireNotifications) IconButton(
? IconButton( icon: const Icon(Icons.delete),
icon: Icon(Icons.delete), onPressed: () => _showConfirmDialog(view)) else null
onPressed: () => _showConfirmDialog(view)) ].where((Widget? w) => w != null).cast<Widget>().toList()),
: null
]).where((w) => w != null).cast<Widget>().toList()),
body: !view.isLoaded body: !view.isLoaded
? new FiresSpinner() ? const FiresSpinner()
: !hasFireNotifications : !hasFireNotifications
? Padding( ? Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
child: new Card( child: Card(
child: new Padding( child: Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
child: new CenteredColumn(children: <Widget>[ child: CenteredColumn(children: <Widget>[
new Icon(Icons.notifications_none, const Icon(Icons.notifications_none,
size: 150.0, color: Colors.black26), size: 150.0, color: Colors.black26),
new SizedBox(height: 20.0), const SizedBox(height: 20.0),
new Text( Text(
S S
.of(context) .of(context)
.fireNotificationsDescription, .fireNotificationsDescription,
textAlign: TextAlign.center, textAlign: TextAlign.center,
textScaleFactor: 1.1, textScaleFactor: 1.1,
style: new TextStyle( style: const TextStyle(
height: 1.3, color: Colors.black45)) height: 1.3, color: Colors.black45))
])))) ]))))
: _buildSavedFireNotifications( : _buildSavedFireNotifications(
@ -231,29 +230,29 @@ class _FireNotificationListState extends State<FireNotificationList> {
void gotoMap( void gotoMap(
Store<AppState> store, FireNotification notif, BuildContext context) { Store<AppState> store, FireNotification notif, BuildContext context) {
store.dispatch(new ShowFireNotificationMapAction(notif)); store.dispatch(ShowFireNotificationMapAction(notif));
Navigator.push( Navigator.push(
context, new MaterialPageRoute(builder: (context) => new genericMap())); context, MaterialPageRoute(builder: (BuildContext context) => const genericMap()));
} }
_showConfirmDialog(_ViewModel view) { Future<void> _showConfirmDialog(_ViewModel view) {
return showDialog<Null>( return showDialog<void>(
context: context, context: context,
barrierDismissible: false, // user must tap button! barrierDismissible: false, // user must tap button!
builder: (BuildContext context) { builder: (BuildContext context) {
return new AlertDialog( return AlertDialog(
title: new Text(S.of(context).areYouSureTitle), title: Text(S.of(context).areYouSureTitle),
content: new SingleChildScrollView( content: SingleChildScrollView(
child: new ListBody( child: ListBody(
children: <Widget>[ children: <Widget>[
new Text( Text(
S.of(context).deleteAllFireNotificationsAlertDescription) S.of(context).deleteAllFireNotificationsAlertDescription)
], ],
), ),
), ),
actions: <Widget>[ actions: <Widget>[
new TextButton( TextButton(
child: new Text(S.of(context).DELETE), child: Text(S.of(context).DELETE),
onPressed: () { onPressed: () {
view.onDeleteAll(); view.onDeleteAll();
Navigator.of(context).pop(); Navigator.of(context).pop();

View file

@ -20,7 +20,7 @@ import 'theme.dart';
import 'themeDev.dart'; import 'themeDev.dart';
class FiresApp extends StatefulWidget { class FiresApp extends StatefulWidget {
FiresApp(this.store); const FiresApp(this.store, {super.key});
final Store<AppState> store; final Store<AppState> store;
@ -29,48 +29,48 @@ class FiresApp extends StatefulWidget {
} }
class _FiresAppState extends State<FiresApp> { class _FiresAppState extends State<FiresApp> {
// globals.getIt.registerSingleton
_FiresAppState(this.store);
final GlobalKey<NavigatorState> navigatorKey = final GlobalKey<NavigatorState> navigatorKey =
new GlobalKey<NavigatorState>(); GlobalKey<NavigatorState>();
static final WidgetBuilder introWidget = (context) => new IntroPage(); static Widget introWidget(BuildContext context) => IntroPage();
static final WidgetBuilder continueWidget = (context) => new HomePage(); static Widget continueWidget(BuildContext context) => const HomePage();
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
IntroPage.routeName: introWidget, IntroPage.routeName: introWidget,
HomePage.routeName: continueWidget, HomePage.routeName: continueWidget,
PrivacyPage.routeName: (BuildContext context) => new PrivacyPage(context), PrivacyPage.routeName: (BuildContext context) => PrivacyPage(context),
ActiveFiresPage.routeName: (BuildContext context) => new ActiveFiresPage(), ActiveFiresPage.routeName: (BuildContext context) => const ActiveFiresPage(),
Sandbox.routeName: (BuildContext context) => new Sandbox(), Sandbox.routeName: (BuildContext context) => const Sandbox(),
FireAlert.routeName: (BuildContext context) => new FireAlert(), FireAlert.routeName: (BuildContext context) => const FireAlert(),
SupportPage.routeName: (BuildContext context) => new SupportPage(), SupportPage.routeName: (BuildContext context) => const SupportPage(),
FireNotificationList.routeName: (BuildContext context) => FireNotificationList.routeName: (BuildContext context) =>
new FireNotificationList(), const FireNotificationList(),
MonitoredAreasPage.routeName: (BuildContext context) => MonitoredAreasPage.routeName: (BuildContext context) =>
new MonitoredAreasPage() const MonitoredAreasPage()
}; };
final Store<AppState> store; final Store<AppState> store;
// globals.getIt.registerSingleton
_FiresAppState(this.store);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
StatefulWidget home = new MaterialAppWithIntroHome( final StatefulWidget home = MaterialAppWithIntroHome(
introWidget, continueWidget, 'showInitialWizard-2018-06-27-01'); introWidget, continueWidget, 'showInitialWizard-2018-06-27-01');
return new StoreProvider<AppState>( return StoreProvider<AppState>(
store: this.store, store: store,
child: new MaterialApp( child: MaterialApp(
navigatorKey: navigatorKey, navigatorKey: navigatorKey,
localizationsDelegates: [ localizationsDelegates: <LocalizationsDelegate<dynamic>> [
S.delegate, S.delegate,
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
], ],
supportedLocales: S.delegate.supportedLocales, supportedLocales: S.delegate.supportedLocales,
localeResolutionCallback: localeResolutionCallback:
S.delegate.resolution(fallback: new Locale("en", "")), S.delegate.resolution(fallback: const Locale('en', '')),
home: home, home: home,
onGenerateTitle: (context) { onGenerateTitle: (BuildContext context) {
print('MaterialApp onGenerateTitle'); print('MaterialApp onGenerateTitle');
return S.of(context).appName; return S.of(context).appName;
}, },

View file

@ -3,8 +3,10 @@ import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'colors.dart'; import 'colors.dart';
class FiresSpinner extends StatelessWidget { class FiresSpinner extends StatelessWidget {
const FiresSpinner({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new SpinKitPulse(color: fires600); return const SpinKitPulse(color: fires600);
} }
} }

View file

@ -1,11 +1,11 @@
import 'dart:core'; import 'dart:core';
import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:fires_flutter/models/yourLocation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:redux/src/store.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'attributionMapPlugin.dart'; import 'attributionMapPlugin.dart';
@ -21,6 +21,9 @@ import 'globals.dart' as globals;
import 'layerSelectorMapPlugin.dart'; import 'layerSelectorMapPlugin.dart';
import 'locationUtils.dart'; import 'locationUtils.dart';
import 'models/appState.dart'; import 'models/appState.dart';
import 'models/falsePositiveTypes.dart';
import 'models/fireNotification.dart';
import 'models/yourLocation.dart';
import 'redux/actions.dart'; import 'redux/actions.dart';
import 'sentryReport.dart'; import 'sentryReport.dart';
import 'slider.dart'; import 'slider.dart';
@ -28,21 +31,8 @@ import 'zoomMapPlugin.dart';
@immutable @immutable
class _ViewModel { class _ViewModel {
final String serverUrl;
final String lang;
final FireMapState mapState;
final OnSubscribeFunction onSubs;
final OnSubscribeConfirmedFunction onSubsConfirmed;
final OnUnSubscribeFunction onUnSubs;
final OnSubscribeDistanceChangeFunction onSlide;
final OnLocationEdit onEdit;
final OnLocationEditConfirm onEditConfirm;
final OnLocationEditCancel onEditCancel;
final OnLocationEditing onEditing;
final OnFalsePositive onFalsePositive;
final OnFirePressedInMap onFirePressed;
_ViewModel( const _ViewModel(
{required this.mapState, {required this.mapState,
required this.serverUrl, required this.serverUrl,
required this.lang, required this.lang,
@ -56,6 +46,19 @@ class _ViewModel {
required this.onFalsePositive, required this.onFalsePositive,
required this.onFirePressed, required this.onFirePressed,
required this.onEditCancel}); required this.onEditCancel});
final String serverUrl;
final String lang;
final FireMapState mapState;
final OnSubscribeFunction onSubs;
final OnSubscribeConfirmedFunction onSubsConfirmed;
final OnUnSubscribeFunction onUnSubs;
final OnSubscribeDistanceChangeFunction onSlide;
final OnLocationEdit onEdit;
final OnLocationEditConfirm onEditConfirm;
final OnLocationEditCancel onEditCancel;
final OnLocationEditing onEditing;
final OnFalsePositive onFalsePositive;
final OnFirePressedInMap onFirePressed;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@ -71,6 +74,8 @@ class _ViewModel {
} }
class genericMap extends StatefulWidget { class genericMap extends StatefulWidget {
const genericMap({super.key});
@override @override
_genericMapState createState() => _genericMapState(); _genericMapState createState() => _genericMapState();
} }
@ -78,49 +83,49 @@ class genericMap extends StatefulWidget {
class _genericMapState extends State<genericMap> { class _genericMapState extends State<genericMap> {
// This needs to be stateful so when resizes don't get a new globalkey // This needs to be stateful so when resizes don't get a new globalkey
// https://github.com/flutter/flutter/issues/1632#issuecomment-180478202 // https://github.com/flutter/flutter/issues/1632#issuecomment-180478202
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
YourLocation? _location; YourLocation? _location;
YourLocation? _initialLocation; YourLocation? _initialLocation;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new StoreConnector<AppState, _ViewModel>( return StoreConnector<AppState, _ViewModel>(
distinct: true, distinct: true,
onInitialBuild: (store) { onInitialBuild: (_ViewModel store) {
_initialLocation = _location?.copyWith(); _initialLocation = _location?.copyWith();
}, },
converter: (store) { converter: (Store<AppState> store) {
print('New map viewer'); print('New map viewer');
return new _ViewModel( return _ViewModel(
onSubs: (loc) { onSubs: (YourLocation loc) {
store.dispatch(new SubscribeAction()); store.dispatch(SubscribeAction());
}, },
onSubsConfirmed: (loc) { onSubsConfirmed: (YourLocation loc) {
loc.subscribed = true; loc.subscribed = true;
store.dispatch(new SubscribeConfirmAction(loc)); store.dispatch(SubscribeConfirmAction(loc));
}, },
onUnSubs: (loc) { onUnSubs: (YourLocation loc) {
loc.subscribed = false; loc.subscribed = false;
store.dispatch(new UnSubscribeAction(loc)); store.dispatch(UnSubscribeAction(loc));
}, },
onSlide: (loc) { onSlide: (YourLocation loc) {
store.dispatch(new UpdateYourLocationMapAction(loc)); store.dispatch(UpdateYourLocationMapAction(loc));
}, },
onEdit: (loc) => store.dispatch(new EditYourLocationAction(loc)), onEdit: (YourLocation loc) => store.dispatch(EditYourLocationAction(loc)),
onEditing: (loc) { onEditing: (YourLocation loc) {
store.dispatch(new UpdateYourLocationMapAction(loc)); store.dispatch(UpdateYourLocationMapAction(loc));
}, },
onEditCancel: (loc) => onEditCancel: (YourLocation loc) =>
store.dispatch(new EditCancelYourLocationAction(loc)), store.dispatch(EditCancelYourLocationAction(loc)),
onEditConfirm: (loc) { onEditConfirm: (YourLocation loc) {
store.dispatch(new UpdateYourLocationAction(loc)); store.dispatch(UpdateYourLocationAction(loc));
store.dispatch(new UpdateYourLocationMapAction(loc)); store.dispatch(UpdateYourLocationMapAction(loc));
store.dispatch(new EditConfirmYourLocationAction(loc)); store.dispatch(EditConfirmYourLocationAction(loc));
}, },
onFalsePositive: (notif, type) => store onFalsePositive: (FireNotification notif, FalsePositiveType type) => store
.dispatch(new MarkFireAsFalsePositiveAction(notif, type)), .dispatch(MarkFireAsFalsePositiveAction(notif, type)),
onFirePressed: (LatLng latLng, DateTime when, type) { onFirePressed: (LatLng latLng, DateTime when, String type) {
_showFireDialog(latLng, when, type); _showFireDialog(latLng, when, type);
}, },
serverUrl: store.state.serverUrl, serverUrl: store.state.serverUrl,
@ -128,24 +133,23 @@ class _genericMapState extends State<genericMap> {
lang: store.state.user.lang, lang: store.state.user.lang,
mapState: store.state.fireMapState); mapState: store.state.fireMapState);
}, },
builder: (context, view) { builder: (BuildContext context, _ViewModel view) {
YourLocation? location = view.mapState.yourLocation; final YourLocation? location = view.mapState.yourLocation;
_location = location?.copyWith(); _location = location?.copyWith();
print('New map builder with ${_location?.description}'); print('New map builder with ${_location?.description}');
FireMapState mapState = view.mapState; final FireMapState mapState = view.mapState;
FireMapStatus status = mapState.status; final FireMapStatus status = mapState.status;
FireMapLayer layer = mapState.layer; final FireMapLayer layer = mapState.layer;
print('Build map with status: $status and layer $layer'); print('Build map with status: $status and layer $layer');
double maxZoom = 18.0; // works? const double maxZoom = 18.0; // works?
MapOptions mapOptions = new MapOptions( final MapOptions mapOptions = MapOptions(
initialCenter: initialCenter:
new LatLng(_location?.lat ?? 0.0, _location?.lon ?? 0.0), LatLng(_location?.lat ?? 0.0, _location?.lon ?? 0.0),
initialZoom: 13.0,
maxZoom: maxZoom, maxZoom: maxZoom,
onTap: (tapPosition, latLng) { onTap: (TapPosition tapPosition, LatLng latLng) {
if (status == FireMapStatus.edit && _location != null) { if (status == FireMapStatus.edit && _location != null) {
_location = _location! _location = _location!
.copyWith(lat: latLng.latitude, lon: latLng.longitude); .copyWith(lat: latLng.latitude, lon: latLng.longitude);
@ -161,19 +165,19 @@ class _genericMapState extends State<genericMap> {
// mapController.fitBounds(bounds); // mapController.fitBounds(bounds);
// mapController.center // mapController.center
final btnText = status == FireMapStatus.view final String btnText = status == FireMapStatus.view
? S.of(context).toFiresNotifications ? S.of(context).toFiresNotifications
: status == FireMapStatus.subscriptionConfirm : status == FireMapStatus.subscriptionConfirm
? S.of(context).confirm ? S.of(context).confirm
: S.of(context).unsubscribe; : S.of(context).unsubscribe;
final btnIcon = status == FireMapStatus.view final IconData btnIcon = status == FireMapStatus.view
? Icons.notifications_active ? Icons.notifications_active
: status == FireMapStatus.subscriptionConfirm : status == FireMapStatus.subscriptionConfirm
? Icons.check ? Icons.check
: Icons.notifications_off; : Icons.notifications_off;
String baseLayer; String baseLayer;
List<String> subdomains = []; List<String> subdomains = <String>[];
String attribution; String attribution;
switch (layer) { switch (layer) {
case FireMapLayer.osmc: case FireMapLayer.osmc:
@ -188,14 +192,14 @@ class _genericMapState extends State<genericMap> {
/* tiles from: https://github.com/dceejay/RedMap/blob/1914ed3b9ce4e8a496049849a93282730b4fff02/worldmap/index.html */ /* tiles from: https://github.com/dceejay/RedMap/blob/1914ed3b9ce4e8a496049849a93282730b4fff02/worldmap/index.html */
switch (layer) { switch (layer) {
case FireMapLayer.osmc: case FireMapLayer.osmc:
subdomains = ['a', 'b', 'c']; subdomains = <String>['a', 'b', 'c'];
baseLayer = baseLayer =
'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png'; 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png';
break; break;
case FireMapLayer.osmcGrey: case FireMapLayer.osmcGrey:
baseLayer = baseLayer =
'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png'; 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png';
subdomains = ['a', 'b', 'c', 'd']; subdomains = <String>['a', 'b', 'c', 'd'];
break; break;
case FireMapLayer.esri: case FireMapLayer.esri:
baseLayer = baseLayer =
@ -209,29 +213,27 @@ class _genericMapState extends State<genericMap> {
baseLayer = baseLayer =
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Shaded_Relief/MapServer/tile/{z}/{y}/{x}'; 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Shaded_Relief/MapServer/tile/{z}/{y}/{x}';
} }
FlutterMap map = new FlutterMap( final FlutterMap map = FlutterMap(
options: mapOptions, options: mapOptions,
children: [ children: <Widget>[
new TileLayer( TileLayer(
maxZoom: maxZoom, maxZoom: maxZoom,
urlTemplate: baseLayer, urlTemplate: baseLayer,
subdomains: subdomains, subdomains: subdomains,
userAgentPackageName: 'com.example.fires_flutter', userAgentPackageName: 'com.example.fires_flutter',
additionalOptions: { additionalOptions: const <String, String> {
// 'opacity': '0.1', // 'opacity': '0.1',
}, },
), ),
globals.isDevelopment if (globals.isDevelopment) const ZoomMapPluginWidget() else const DummyMapPluginWidget(),
? new ZoomMapPluginWidget() const CompassMapPluginWidget(),
: new DummyMapPluginWidget(), MarkerLayer(
new CompassMapPluginWidget(),
new MarkerLayer(
markers: buildMarkers( markers: buildMarkers(
mapState.status == FireMapStatus.viewFireNotification && mapState.status == FireMapStatus.viewFireNotification &&
mapState.fireNotification != null mapState.fireNotification != null
? new LatLng(mapState.fireNotification!.lat, ? LatLng(mapState.fireNotification!.lat,
mapState.fireNotification!.lon) mapState.fireNotification!.lon)
: new LatLng(_location!.lat, _location!.lon), : LatLng(_location!.lat, _location!.lon),
mapState.fires, mapState.fires,
mapState.industries, mapState.industries,
mapState.falsePos, mapState.falsePos,
@ -239,8 +241,8 @@ class _genericMapState extends State<genericMap> {
view.onFirePressed), view.onFirePressed),
), ),
// new AttributionPluginWidget(text: "© OpenStreetMap contributors"), // new AttributionPluginWidget(text: "© OpenStreetMap contributors"),
new LayerSelectorMapPluginWidget(), const LayerSelectorMapPluginWidget(),
new AttributionPluginWidget(text: attribution), AttributionPluginWidget(text: attribution),
], ],
); );
// mapController. // mapController.
@ -250,35 +252,33 @@ class _genericMapState extends State<genericMap> {
; ;
}); */ }); */
// Do something with it // Do something with it
return new Scaffold( return Scaffold(
key: _scaffoldKey, key: _scaffoldKey,
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
appBar: new AppBar( appBar: AppBar(
title: status == FireMapStatus.edit title: status == FireMapStatus.edit
? new TextField( ? TextField(
// autofocus: true, // autofocus: true,
key: new Key('LocationDescField'), key: const Key('LocationDescField'),
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
controller: TextEditingController.fromValue(
decoration: new InputDecoration(), TextEditingValue(
controller: new TextEditingController.fromValue(
new TextEditingValue(
text: _location!.description, text: _location!.description,
selection: new TextSelection.collapsed( selection: TextSelection.collapsed(
offset: _location!.description.length))), offset: _location!.description.length))),
onChanged: (newDesc) { onChanged: (String newDesc) {
debugPrint("OnChanged"); debugPrint('OnChanged');
_location = _location!.copyWith(description: newDesc); _location = _location!.copyWith(description: newDesc);
}, },
onSubmitted: (newDesc) { onSubmitted: (String newDesc) {
debugPrint("OnSubmitted"); debugPrint('OnSubmitted');
_location = _location!.copyWith(description: newDesc); _location = _location!.copyWith(description: newDesc);
view.onEditConfirm(_location!); view.onEditConfirm(_location!);
}, },
) )
: status == FireMapStatus.viewFireNotification : status == FireMapStatus.viewFireNotification
? new Text(S.of(context).fireNotificationTitle) ? Text(S.of(context).fireNotificationTitle)
: new Text(_location!.description), : Text(_location!.description),
actions: buildAppBarActions(status, view, _location!), actions: buildAppBarActions(status, view, _location!),
), ),
floatingActionButton: status == FireMapStatus.edit || floatingActionButton: status == FireMapStatus.edit ||
@ -302,9 +302,9 @@ class _genericMapState extends State<genericMap> {
} }
}, },
// https://github.com/flutter/flutter/issues/17583 // https://github.com/flutter/flutter/issues/17583
heroTag: "firesmap" + _location!.id.hexString, heroTag: 'firesmap${_location!.id.hexString}',
icon: new Icon(btnIcon, color: fires600), icon: Icon(btnIcon, color: fires600),
label: new Text( label: Text(
btnText, btnText,
style: const TextStyle(color: fires600), style: const TextStyle(color: fires600),
), ),
@ -312,20 +312,20 @@ class _genericMapState extends State<genericMap> {
), ),
floatingActionButtonLocation: floatingActionButtonLocation:
FloatingActionButtonLocation.centerFloat, FloatingActionButtonLocation.centerFloat,
bottomNavigationBar: new GenericMapBottom( bottomNavigationBar: GenericMapBottom(
onSave: () => view.onEditConfirm(_location!), onSave: () => view.onEditConfirm(_location!),
onCancel: () => onCancel: () =>
view.onEditCancel(_initialLocation ?? _location!), view.onEditCancel(_initialLocation ?? _location!),
onFalsePositive: (sealed, type) => onFalsePositive: (FireNotification sealed, FalsePositiveType type) =>
view.onFalsePositive(sealed, type), view.onFalsePositive(sealed, type),
state: view.mapState, state: view.mapState,
scaffoldKey: _scaffoldKey, scaffoldKey: _scaffoldKey,
), ),
body: LayoutBuilder( body: LayoutBuilder(
builder: (context, constraints) => builder: (BuildContext context, BoxConstraints constraints) =>
Stack(fit: StackFit.expand, children: <Widget>[ Stack(fit: StackFit.expand, children: <Widget>[
// Material(color: Colors.yellowAccent), // Material(color: Colors.yellowAccent),
new Opacity( Opacity(
opacity: opacity:
status == FireMapStatus.subscriptionConfirm || status == FireMapStatus.subscriptionConfirm ||
status == FireMapStatus.edit status == FireMapStatus.edit
@ -336,7 +336,7 @@ class _genericMapState extends State<genericMap> {
top: constraints.maxHeight - 200, top: constraints.maxHeight - 200,
right: 10.0, right: 10.0,
left: 10.0, left: 10.0,
child: new CenteredRow( child: CenteredRow(
// Fit sample: // Fit sample:
// https://github.com/apptreesoftware/flutter_map/blob/master/flutter_map_example/lib/pages/map_controller.dart // https://github.com/apptreesoftware/flutter_map/blob/master/flutter_map_example/lib/pages/map_controller.dart
children: status == children: status ==
@ -345,18 +345,18 @@ class _genericMapState extends State<genericMap> {
_location != null && _location != null &&
_location!.subscribed) _location!.subscribed)
? <Widget>[ ? <Widget>[
new FireDistanceSlider( FireDistanceSlider(
initialValue: initialValue:
(_location?.distance ?? 0) _location?.distance ?? 0
.round(), ,
onSlide: (distance) { onSlide: (int distance) {
if (_location != null) { if (_location != null) {
_location!.distance = distance; _location!.distance = distance;
view.onSlide(_location!); view.onSlide(_location!);
} }
}) })
] ]
: []), : <Widget>[]),
) )
]))); ])));
}); });
@ -368,20 +368,20 @@ class _genericMapState extends State<genericMap> {
case FireMapStatus.view: case FireMapStatus.view:
case FireMapStatus.unsubscribe: case FireMapStatus.unsubscribe:
return <Widget>[ return <Widget>[
new IconButton( IconButton(
icon: new Icon(Icons.edit), icon: const Icon(Icons.edit),
onPressed: () => view.onEdit(location)) onPressed: () => view.onEdit(location))
]; ];
case FireMapStatus.edit: case FireMapStatus.edit:
return <Widget>[ return <Widget>[
new IconButton( IconButton(
icon: new Icon(Icons.save), icon: const Icon(Icons.save),
onPressed: () => view.onEditConfirm(_location!)) onPressed: () => view.onEditConfirm(_location!))
]; ];
case FireMapStatus.viewFireNotification: case FireMapStatus.viewFireNotification:
return <Widget>[ return <Widget>[
new IconButton( IconButton(
icon: new Icon(Icons.share), icon: const Icon(Icons.share),
onPressed: () { onPressed: () {
Share.share( Share.share(
'${view.mapState.fireNotification?.description ?? 'Fire'}. ${view.serverUrl}fire/${view.mapState.fireNotification?.sealed ?? ''}'); '${view.mapState.fireNotification?.description ?? 'Fire'}. ${view.serverUrl}fire/${view.mapState.fireNotification?.sealed ?? ''}');
@ -399,15 +399,15 @@ class _genericMapState extends State<genericMap> {
List<dynamic> industries, List<dynamic> industries,
bool isNotif, bool isNotif,
OnFirePressedInMap onFirePressed) { OnFirePressedInMap onFirePressed) {
List<Marker> markers = []; final List<Marker> markers = <Marker>[];
print( print(
'building markers: fires: ${fires.length} falsePos: ${falsePosList.length} industries: ${industries.length}, isNotif: $isNotif '); 'building markers: fires: ${fires.length} falsePos: ${falsePosList.length} industries: ${industries.length}, isNotif: $isNotif ');
// const calibrate = false; // useful when we change the fire icons size // const calibrate = false; // useful when we change the fire icons size
falsePosList.forEach((falsePos) { for (final falsePos in falsePosList) {
try { try {
var coords = falsePos['geo']['coordinates'] as List<dynamic>; final List<dynamic> coords = falsePos['geo']['coordinates'] as List<dynamic>;
// print('false pos: ${coords}'); // print('false pos: ${coords}');
var loc = LatLng( final LatLng loc = LatLng(
(coords[1] as num).toDouble(), (coords[0] as num).toDouble()); (coords[1] as num).toDouble(), (coords[0] as num).toDouble());
markers.add(FireMarker(loc, FireMarkType.falsePos, () { markers.add(FireMarker(loc, FireMarkType.falsePos, () {
_showFalsePositiveDialog(loc); _showFalsePositiveDialog(loc);
@ -417,12 +417,12 @@ class _genericMapState extends State<genericMap> {
print('Failed to process $falsePos'); print('Failed to process $falsePos');
reportError(e, stackTrace); reportError(e, stackTrace);
} }
}); }
industries.forEach((industry) { for (final industry in industries) {
try { try {
// print(fire['geo']['coordinates']); // print(fire['geo']['coordinates']);
var coords = industry['geo']['coordinates'] as List<dynamic>; final List<dynamic> coords = industry['geo']['coordinates'] as List<dynamic>;
var loc = LatLng( final LatLng loc = LatLng(
(coords[1] as num).toDouble(), (coords[0] as num).toDouble()); (coords[1] as num).toDouble(), (coords[0] as num).toDouble());
markers.add(FireMarker(loc, FireMarkType.industry, () { markers.add(FireMarker(loc, FireMarkType.industry, () {
_showIndustryDialog(loc); _showIndustryDialog(loc);
@ -432,14 +432,14 @@ class _genericMapState extends State<genericMap> {
print('Failed to process $industry'); print('Failed to process $industry');
reportError(e, stackTrace); reportError(e, stackTrace);
} }
}); }
fires.forEach((fire) { for (final fire in fires) {
try { try {
var loc = new LatLng( final LatLng loc = LatLng(
(fire['lat'] as num).toDouble(), (fire['lon'] as num).toDouble()); (fire['lat'] as num).toDouble(), (fire['lon'] as num).toDouble());
markers.add(FireMarker(loc, FireMarkType.fire, () { markers.add(FireMarker(loc, FireMarkType.fire, () {
onFirePressed(loc, DateTime.parse(fire['when'].toString()), onFirePressed(loc, DateTime.parse(fire['when'].toString()),
(fire['type'] as String)); fire['type'] as String);
print('fire $fire pressed'); print('fire $fire pressed');
})); }));
markers.add(FireMarker(loc, FireMarkType.pixel)); markers.add(FireMarker(loc, FireMarkType.pixel));
@ -447,7 +447,7 @@ class _genericMapState extends State<genericMap> {
print('Failed to process $fire'); print('Failed to process $fire');
reportError(e, stackTrace); reportError(e, stackTrace);
} }
}); }
markers.add( markers.add(
FireMarker(pos, isNotif ? FireMarkType.fire : FireMarkType.position)); FireMarker(pos, isNotif ? FireMarkType.fire : FireMarkType.position));
// if (calibrate) markers.add(FireMarker(pos, FireMarkType.pixel)); // if (calibrate) markers.add(FireMarker(pos, FireMarkType.pixel));
@ -455,20 +455,20 @@ class _genericMapState extends State<genericMap> {
} }
void _showFireDialog(LatLng pos, DateTime date, String type) { void _showFireDialog(LatLng pos, DateTime date, String type) {
var when = Moment.fromDate(date).fromNow(context); final String when = Moment.fromDate(date).fromNow(context);
getReverseLocation(lat: pos.latitude, lon: pos.longitude) getReverseLocation(lat: pos.latitude, lon: pos.longitude)
.then((reverseLoc) { .then((String reverseLoc) {
String by = type == 'vecinal' final String by = type == 'vecinal'
? S.of(context).byOurUsers ? S.of(context).byOurUsers
: S.of(context).byNASAsatellites; : S.of(context).byNASAsatellites;
String fireDesc = final String fireDesc =
S.of(context).additionalInfoAboutFire(reverseLoc, when, by); S.of(context).additionalInfoAboutFire(reverseLoc, when, by);
showDialog<bool>( showDialog<bool>(
context: _scaffoldKey.currentContext!, context: _scaffoldKey.currentContext!,
builder: (_) => new AlertDialog( builder: (_) => AlertDialog(
content: new Text(fireDesc), content: Text(fireDesc),
actions: <Widget>[ actions: <Widget>[
new TextButton( TextButton(
child: Text(S.of(context).CLOSE), child: Text(S.of(context).CLOSE),
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
@ -481,17 +481,17 @@ class _genericMapState extends State<genericMap> {
void _showIndustryDialog(LatLng pos) { void _showIndustryDialog(LatLng pos) {
getReverseLocation(lat: pos.latitude, lon: pos.longitude) getReverseLocation(lat: pos.latitude, lon: pos.longitude)
.then((reverseLoc) { .then((String reverseLoc) {
String industryDesc = '${S.of(context).itSeemsAIndustry}\n\n' final String industryDesc = '${S.of(context).itSeemsAIndustry}\n\n'
'Type: Industry\n' 'Type: Industry\n'
'Location: $reverseLoc'; 'Location: $reverseLoc';
showDialog<bool>( showDialog<bool>(
context: _scaffoldKey.currentContext!, context: _scaffoldKey.currentContext!,
builder: (_) => new AlertDialog( builder: (_) => AlertDialog(
title: Text(S.of(context).notAWildfire), title: Text(S.of(context).notAWildfire),
content: new Text(industryDesc), content: Text(industryDesc),
actions: <Widget>[ actions: <Widget>[
new TextButton( TextButton(
child: Text(S.of(context).CLOSE), child: Text(S.of(context).CLOSE),
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
@ -504,17 +504,17 @@ class _genericMapState extends State<genericMap> {
void _showFalsePositiveDialog(LatLng pos) { void _showFalsePositiveDialog(LatLng pos) {
getReverseLocation(lat: pos.latitude, lon: pos.longitude) getReverseLocation(lat: pos.latitude, lon: pos.longitude)
.then((reverseLoc) { .then((String reverseLoc) {
String falseDesc = '${S.of(context).itSeemsNotAtForesFire}\n\n' final String falseDesc = '${S.of(context).itSeemsNotAtForesFire}\n\n'
'Type: False Positive\n' 'Type: False Positive\n'
'Location: $reverseLoc'; 'Location: $reverseLoc';
showDialog<bool>( showDialog<bool>(
context: _scaffoldKey.currentContext!, context: _scaffoldKey.currentContext!,
builder: (_) => new AlertDialog( builder: (_) => AlertDialog(
title: Text(S.of(context).notAWildfire), title: Text(S.of(context).notAWildfire),
content: new Text(falseDesc), content: Text(falseDesc),
actions: <Widget>[ actions: <Widget>[
new TextButton( TextButton(
child: Text(S.of(context).CLOSE), child: Text(S.of(context).CLOSE),
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);

View file

@ -1,8 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:fires_flutter/models/fireNotification.dart';
import 'package:fires_flutter/models/yourLocation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'colors.dart'; import 'colors.dart';
@ -12,90 +10,87 @@ import 'generated/i18n.dart';
import 'models/appState.dart'; import 'models/appState.dart';
import 'models/falsePositiveTypes.dart'; import 'models/falsePositiveTypes.dart';
import 'models/fireMapState.dart'; import 'models/fireMapState.dart';
import 'models/fireNotification.dart';
import 'models/yourLocation.dart';
typedef void OnSave(); typedef OnSave = void Function();
typedef void OnCancel(); typedef OnCancel = void Function();
typedef void OnFalsePositive(FireNotification notif, FalsePositiveType type); typedef OnFalsePositive = void Function(FireNotification notif, FalsePositiveType type);
class GenericMapBottom extends StatelessWidget { class GenericMapBottom extends StatelessWidget {
const GenericMapBottom(
{super.key, required this.onSave,
required this.onCancel,
required this.onFalsePositive,
required this.state,
required this.scaffoldKey});
final OnSave onSave; final OnSave onSave;
final OnCancel onCancel; final OnCancel onCancel;
final OnFalsePositive onFalsePositive; final OnFalsePositive onFalsePositive;
final FireMapState state; final FireMapState state;
final GlobalKey<ScaffoldState> scaffoldKey; final GlobalKey<ScaffoldState> scaffoldKey;
GenericMapBottom(
{required this.onSave,
required this.onCancel,
required this.onFalsePositive,
required this.state,
required this.scaffoldKey});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final YourLocation? locOrNull = state.yourLocation; final YourLocation? locOrNull = state.yourLocation;
if (locOrNull == null) { if (locOrNull == null) {
return SizedBox.shrink(); return const SizedBox.shrink();
} }
final YourLocation loc = locOrNull; final YourLocation loc = locOrNull;
int kmAround = loc.distance; final int kmAround = loc.distance;
return new CustomBottomAppBar( return CustomBottomAppBar(
fabLocation: FloatingActionButtonLocation.centerFloat, fabLocation: FloatingActionButtonLocation.centerFloat,
showNotch: false,
color: fires100, color: fires100,
mainAxisAlignment: MainAxisAlignment.center,
actions: buildActionList(loc, context, kmAround, scaffoldKey)); actions: buildActionList(loc, context, kmAround, scaffoldKey));
} }
List<Widget> buildActionList(YourLocation loc, BuildContext context, List<Widget> buildActionList(YourLocation loc, BuildContext context,
int kmAround, GlobalKey<ScaffoldState> scaffoldState) { int kmAround, GlobalKey<ScaffoldState> scaffoldState) {
List<Widget> actionList = []; final List<Widget> actionList = <Widget>[];
switch (state.status) { switch (state.status) {
case FireMapStatus.edit: case FireMapStatus.edit:
actionList.add(TextButton( actionList.add(TextButton(
onPressed: onSave, onPressed: onSave,
child: new Text(S.of(context).SAVE, child: Text(S.of(context).SAVE,
style: Theme.of(context).textTheme.labelLarge))); style: Theme.of(context).textTheme.labelLarge)));
actionList.add(TextButton( actionList.add(TextButton(
onPressed: onCancel, onPressed: onCancel,
child: new Text(S.of(context).CANCEL, child: Text(S.of(context).CANCEL,
style: Theme.of(context).textTheme.labelLarge))); style: Theme.of(context).textTheme.labelLarge)));
break; break;
case FireMapStatus.subscriptionConfirm: case FireMapStatus.subscriptionConfirm:
break; break;
case FireMapStatus.viewFireNotification: case FireMapStatus.viewFireNotification:
final notif = state.fireNotification; final FireNotification? notif = state.fireNotification;
if (notif != null) { if (notif != null) {
actionList.add(new Flexible( actionList.add(Flexible(
child: new Padding( child: Padding(
padding: new EdgeInsets.all(10.0), padding: const EdgeInsets.all(10.0),
child: new Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: listWithoutNulls(<Widget?>[ children: listWithoutNulls(<Widget?>[
new Text(notif.description), Text(notif.description),
// TODO fire type (neighbout, NASA, etc) // TODOfire type (neighbout, NASA, etc)
new SizedBox(height: 5.0), const SizedBox(height: 5.0),
new Row( Row(
children: <Widget>[ children: <Widget>[
new Icon(Icons.access_time), const Icon(Icons.access_time),
new SizedBox(width: 5.0), const SizedBox(width: 5.0),
new Text(Moment.now().from(context, notif.when)), Text(Moment.now().from(context, notif.when)),
], ],
), ),
state.industries.length > 0 || state.falsePos.length > 0 if (state.industries.isNotEmpty || state.falsePos.isNotEmpty) Padding(
? new Padding( padding: const EdgeInsets.only(top: 10.0),
padding: new EdgeInsets.only(top: 10.0), child: Text(
child: new Text(
S.of(context).itSeemsNotAtForesFire, S.of(context).itSeemsNotAtForesFire,
style: new TextStyle(color: fires600))) style: const TextStyle(color: fires600))) else null,
: null, DropdownButton<FalsePositiveType>(
new DropdownButton<FalsePositiveType>( style: const TextStyle(
style: new TextStyle(
color: Colors.black, color: Colors.black,
// fontSize: 18.0, // fontSize: 18.0,
), ),
hint: new Text(S.of(context).notAWildfire), hint: Text(S.of(context).notAWildfire),
items: FalsePositiveType.values items: FalsePositiveType.values
.map((FalsePositiveType value) { .map((FalsePositiveType value) {
String menuText; String menuText;
@ -110,18 +105,18 @@ class GenericMapBottom extends StatelessWidget {
case FalsePositiveType.falsealarm: case FalsePositiveType.falsealarm:
menuText = S.of(context).itSeemsAFalseAlarm; menuText = S.of(context).itSeemsAFalseAlarm;
} }
return new DropdownMenuItem<FalsePositiveType>( return DropdownMenuItem<FalsePositiveType>(
value: value, child: new Text(menuText)); value: value, child: Text(menuText));
}).toList(), }).toList(),
onChanged: (FalsePositiveType? value) async { onChanged: (FalsePositiveType? value) async {
if (value != null) { if (value != null) {
onFalsePositive(notif, value); onFalsePositive(notif, value);
} }
await new Future.delayed( await Future.delayed(
new Duration(milliseconds: 500)); const Duration(milliseconds: 500));
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context)
.showSnackBar(new SnackBar( .showSnackBar(SnackBar(
content: new Text( content: Text(
S.of(context).thanksForParticipating), S.of(context).thanksForParticipating),
)); ));
}), }),
@ -130,7 +125,7 @@ class GenericMapBottom extends StatelessWidget {
break; break;
case FireMapStatus.unsubscribe: case FireMapStatus.unsubscribe:
case FireMapStatus.view: case FireMapStatus.view:
actionList.add(new Text(state.numFires > 0 actionList.add(Text(state.numFires > 0
? loc.currentNumFires == 1 ? loc.currentNumFires == 1
? S.of(context).fireAroundThisArea(loc.distance.toString()) ? S.of(context).fireAroundThisArea(loc.distance.toString())
: S.of(context).firesAroundThisArea( : S.of(context).firesAroundThisArea(

View file

@ -10,6 +10,8 @@ import 'customMoment.dart';
import 'generated/i18n.dart'; import 'generated/i18n.dart';
class GlobalFiresBottomStats extends StatefulWidget { class GlobalFiresBottomStats extends StatefulWidget {
const GlobalFiresBottomStats({super.key});
@override @override
_GlobalFiresBottomStatsState createState() => _GlobalFiresBottomStatsState(); _GlobalFiresBottomStatsState createState() => _GlobalFiresBottomStatsState();
} }
@ -17,20 +19,20 @@ class GlobalFiresBottomStats extends StatefulWidget {
class _GlobalFiresBottomStatsState extends State<GlobalFiresBottomStats> { class _GlobalFiresBottomStatsState extends State<GlobalFiresBottomStats> {
late String lastCheck; late String lastCheck;
int activeFires = 0; int activeFires = 0;
final firesApiUrl = GetIt.instance<String>(instanceName: "firesApiUrl"); final String firesApiUrl = GetIt.instance<String>(instanceName: 'firesApiUrl');
@override @override
void initState() { void initState() {
super.initState(); super.initState();
http.read(Uri.parse('${firesApiUrl}status/last-fire-check')).then((result) { http.read(Uri.parse('${firesApiUrl}status/last-fire-check')).then((String result) {
try { try {
var now = Moment.now(); final Moment now = Moment.now();
var last = DateTime.parse(json.decode(result)['value'] as String); final DateTime last = DateTime.parse(json.decode(result)['value'] as String);
http http
.read(Uri.parse('${firesApiUrl}status/active-fires-count')) .read(Uri.parse('${firesApiUrl}status/active-fires-count'))
.then((result) { .then((String result) {
try { try {
int count = (json.decode(result)['total'] as num).toInt(); final int count = (json.decode(result)['total'] as num).toInt();
setState(() { setState(() {
lastCheck = now.from(context, last); lastCheck = now.from(context, last);
activeFires = count; activeFires = count;
@ -51,21 +53,19 @@ class _GlobalFiresBottomStatsState extends State<GlobalFiresBottomStats> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<Widget> actionWidgets = <Widget>[]; final List<Widget> actionWidgets = <Widget>[];
if (activeFires > 0) { if (activeFires > 0) {
actionWidgets.add(new Column( actionWidgets.add(Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[
children: [ Text(
new Text(
S.of(context).activeFiresWorldWide(activeFires.toString())), S.of(context).activeFiresWorldWide(activeFires.toString())),
new Text(S.of(context).updatedLastCheck(lastCheck)) Text(S.of(context).updatedLastCheck(lastCheck))
])); ]));
} }
return new CustomBottomAppBar( return CustomBottomAppBar(
fabLocation: FloatingActionButtonLocation.centerDocked, fabLocation: FloatingActionButtonLocation.centerDocked,
showNotch: true, showNotch: true,
color: fires100, color: fires100,
mainAxisAlignment: MainAxisAlignment.center,
actions: actionWidgets); actions: actionWidgets);
} }
} }

View file

@ -2,8 +2,6 @@ import 'dart:async';
import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:fires_flutter/models/fireNotification.dart';
import 'package:fires_flutter/objectIdUtils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
@ -17,12 +15,14 @@ import 'firesSpinner.dart';
import 'generated/i18n.dart'; import 'generated/i18n.dart';
import 'mainDrawer.dart'; import 'mainDrawer.dart';
import 'models/appState.dart'; import 'models/appState.dart';
import 'models/fireNotification.dart';
import 'objectIdUtils.dart';
import 'redux/actions.dart'; import 'redux/actions.dart';
class _ViewModel { class _ViewModel {
final bool isLoaded;
_ViewModel({required this.isLoaded}); _ViewModel({required this.isLoaded});
final bool isLoaded;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@ -36,21 +36,21 @@ class _ViewModel {
} }
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
static const String routeName = '/home';
HomePage(); const HomePage({super.key});
static const String routeName = '/home';
@override @override
_HomePageState createState() => _HomePageState(); _HomePageState createState() => _HomePageState();
} }
class _HomePageState extends State<HomePage> { class _HomePageState extends State<HomePage> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final Store<AppState> store = GetIt.instance<Store<AppState>>(); final Store<AppState> store = GetIt.instance<Store<AppState>>();
final List<FireNotification> newNotifications = []; final List<FireNotification> newNotifications = <FireNotification>[];
// Platform messages are asynchronous, so we initialize in an async method. // Platform messages are asynchronous, so we initialize in an async method.
Future<Null> initConnectivity() async { Future<void> initConnectivity() async {
// Connectivity checking removed - no longer needed // Connectivity checking removed - no longer needed
} }
@ -70,9 +70,9 @@ class _HomePageState extends State<HomePage> {
// Listen for messages when app is in foreground // Listen for messages when app is in foreground
FirebaseMessaging.onMessage.listen((RemoteMessage message) { FirebaseMessaging.onMessage.listen((RemoteMessage message) {
debugPrint( debugPrint(
"onMessage in fireApp (isLoaded: ${store.state.isLoaded}): $message"); 'onMessage in fireApp (isLoaded: ${store.state.isLoaded}): $message');
if (message.data.isNotEmpty) { if (message.data.isNotEmpty) {
final notif = _notifForMessage(message.data, store.state.isLoaded); final FireNotification? notif = _notifForMessage(message.data, store.state.isLoaded);
if (notif != null) { if (notif != null) {
_showItemDialog(message.data, notif); _showItemDialog(message.data, notif);
} }
@ -82,7 +82,7 @@ class _HomePageState extends State<HomePage> {
// Listen for messages when app is opened from background // Listen for messages when app is opened from background
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
debugPrint( debugPrint(
"onMessageOpenedApp (isLoaded: ${store.state.isLoaded}): $message"); 'onMessageOpenedApp (isLoaded: ${store.state.isLoaded}): $message');
if (message.data.isNotEmpty) { if (message.data.isNotEmpty) {
_notifForMessage(message.data, store.state.isLoaded); _notifForMessage(message.data, store.state.isLoaded);
_navigateToItemDetail(message.data); _navigateToItemDetail(message.data);
@ -92,7 +92,7 @@ class _HomePageState extends State<HomePage> {
// Check if app was terminated and opened by notification tap // Check if app was terminated and opened by notification tap
_firebaseMessaging.getInitialMessage().then((RemoteMessage? message) { _firebaseMessaging.getInitialMessage().then((RemoteMessage? message) {
if (message != null) { if (message != null) {
debugPrint("App opened by notification: $message"); debugPrint('App opened by notification: $message');
if (message.data.isNotEmpty) { if (message.data.isNotEmpty) {
_navigateToItemDetail(message.data); _navigateToItemDetail(message.data);
} }
@ -103,39 +103,39 @@ class _HomePageState extends State<HomePage> {
void _getFirebaseToken() { void _getFirebaseToken() {
_firebaseMessaging.getToken().then((String? token) { _firebaseMessaging.getToken().then((String? token) {
if (token != null) { if (token != null) {
store.dispatch(new OnUserTokenAction(token)); store.dispatch(OnUserTokenAction(token));
setState(() {}); setState(() {});
} }
}); });
} }
final _homeFont = const TextStyle( final TextStyle _homeFont = const TextStyle(
fontSize: 50.0, fontSize: 50.0,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
); );
final _btnFont = const TextStyle( final TextStyle _btnFont = const TextStyle(
fontSize: 20.0, fontSize: 20.0,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
); );
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new StoreConnector<AppState, _ViewModel>( return StoreConnector<AppState, _ViewModel>(
distinct: true, distinct: true,
converter: (store) { converter: (Store<AppState> store) {
bool isLoaded = store.state.isLoaded; final bool isLoaded = store.state.isLoaded;
if (isLoaded && newNotifications.isNotEmpty) { if (isLoaded && newNotifications.isNotEmpty) {
newNotifications.forEach((notif) { for (final FireNotification notif in newNotifications) {
store.dispatch(new AddFireNotificationAction(notif)); store.dispatch(AddFireNotificationAction(notif));
}); }
newNotifications.clear(); newNotifications.clear();
} }
return new _ViewModel(isLoaded: store.state.isLoaded); return _ViewModel(isLoaded: store.state.isLoaded);
}, },
builder: (context, view) { builder: (BuildContext context, _ViewModel view) {
return new Scaffold( return Scaffold(
key: _scaffoldKey, key: _scaffoldKey,
drawer: new MainDrawer(context, HomePage.routeName), drawer: MainDrawer(context, HomePage.routeName),
floatingActionButtonLocation: floatingActionButtonLocation:
FloatingActionButtonLocation.centerFloat, FloatingActionButtonLocation.centerFloat,
floatingActionButton: Column( floatingActionButton: Column(
@ -160,7 +160,7 @@ class _HomePageState extends State<HomePage> {
}, },
heroTag: 'notifyFire', heroTag: 'notifyFire',
backgroundColor: fires600, backgroundColor: fires600,
label: new Text(S.of(context).notifyAFire, label: Text(S.of(context).notifyAFire,
style: _btnFont), style: _btnFont),
icon: icon:
const Icon(Icons.notifications_active, size: 32.0), const Icon(Icons.notifications_active, size: 32.0),
@ -168,41 +168,40 @@ class _HomePageState extends State<HomePage> {
), ),
]), ]),
body: !view.isLoaded body: !view.isLoaded
? new FiresSpinner() ? const FiresSpinner()
: new SafeArea( : SafeArea(
child: Center( child: Center(
child: new CenteredColumn(children: <Widget>[ child: CenteredColumn(children: <Widget>[
new Row( Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[ children: <Widget>[
new IconButton( IconButton(
onPressed: () { onPressed: () {
_scaffoldKey.currentState?.openDrawer(); _scaffoldKey.currentState?.openDrawer();
}, },
icon: new Icon(Icons.menu, icon: const Icon(Icons.menu,
size: 30.0, color: Colors.black38)), size: 30.0, color: Colors.black38)),
]), ]),
new Expanded( Expanded(
child: new FractionallySizedBox( child: FractionallySizedBox(
alignment: FractionalOffset.center, alignment: FractionalOffset.center,
heightFactor: 0.7, heightFactor: 0.7,
child: new Image.asset('images/logo-200.png', child: Image.asset('images/logo-200.png',
fit: BoxFit.fitHeight))), fit: BoxFit.fitHeight))),
new Expanded( Expanded(
child: new FractionallySizedBox( child: FractionallySizedBox(
alignment: FractionalOffset.topCenter, alignment: FractionalOffset.topCenter,
heightFactor: 1.0, heightFactor: 1.0,
child: new Column( child: Column(
children: <Widget>[ children: <Widget>[
new Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 10.0, horizontal: 20.0), vertical: 10.0, horizontal: 20.0),
child: FittedBox( child: FittedBox(
child: new Text(S.of(context).appName, fit: BoxFit.scaleDown,
child: Text(S.of(context).appName,
maxLines: 2, maxLines: 2,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: _homeFont), style: _homeFont),
fit: BoxFit.scaleDown,
)), )),
], ],
))) )))
@ -212,14 +211,14 @@ class _HomePageState extends State<HomePage> {
} }
void _showDialog(String message) { void _showDialog(String message) {
final context = _scaffoldKey.currentContext; final BuildContext? context = _scaffoldKey.currentContext;
if (context == null) return; if (context == null) return;
showDialog<bool>( showDialog<bool>(
context: context, context: context,
builder: (_) => new AlertDialog( builder: (_) => AlertDialog(
content: new Text(message), content: Text(message),
actions: <Widget>[ actions: <Widget>[
new TextButton( TextButton(
child: Text(S.of(context).CLOSE), child: Text(S.of(context).CLOSE),
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
@ -230,29 +229,29 @@ class _HomePageState extends State<HomePage> {
} }
void _showItemDialog(Map<String, dynamic> message, FireNotification notif) { void _showItemDialog(Map<String, dynamic> message, FireNotification notif) {
final context = _scaffoldKey.currentContext; final BuildContext? context = _scaffoldKey.currentContext;
if (context == null) return; if (context == null) return;
showDialog<bool>( showDialog<bool>(
context: context, context: context,
builder: (_) => _buildDialog(context, notif), builder: (_) => _buildDialog(context, notif),
).then((bool? shouldNavigate) { ).then((bool? shouldNavigate) {
if (shouldNavigate == true) { if (shouldNavigate ?? false) {
_navigateToItemDetail(message); _navigateToItemDetail(message);
} }
}).catchError((e) => print("$e")); }).catchError((e) => print('$e'));
} }
Widget _buildDialog(BuildContext context, FireNotification item) { Widget _buildDialog(BuildContext context, FireNotification item) {
return new AlertDialog( return AlertDialog(
content: new Text(item.description), content: Text(item.description),
actions: <Widget>[ actions: <Widget>[
new TextButton( TextButton(
child: Text(S.of(context).CLOSE), child: Text(S.of(context).CLOSE),
onPressed: () { onPressed: () {
Navigator.pop(context, false); Navigator.pop(context, false);
}, },
), ),
new TextButton( TextButton(
child: Text(S.of(context).SHOW), child: Text(S.of(context).SHOW),
onPressed: () { onPressed: () {
Navigator.pop(context, true); Navigator.pop(context, true);
@ -263,7 +262,7 @@ class _HomePageState extends State<HomePage> {
} }
void _navigateToItemDetail(Map<String, dynamic> message) { void _navigateToItemDetail(Map<String, dynamic> message) {
final context = _scaffoldKey.currentContext; final BuildContext? context = _scaffoldKey.currentContext;
if (context == null) return; if (context == null) return;
// Clear away dialogs // Clear away dialogs
Navigator.popUntil(context, (Route<dynamic> route) => route is PageRoute); Navigator.popUntil(context, (Route<dynamic> route) => route is PageRoute);
@ -280,7 +279,7 @@ class _HomePageState extends State<HomePage> {
Map<String, dynamic> message, bool isLoaded) { Map<String, dynamic> message, bool isLoaded) {
FireNotification? notif; FireNotification? notif;
try { try {
notif = new FireNotification( notif = FireNotification(
id: objectIdFromJson(message['id'] as String), id: objectIdFromJson(message['id'] as String),
subsId: objectIdFromJson(message['subsId'] as String), subsId: objectIdFromJson(message['subsId'] as String),
lat: double.parse(message['lat'] as String), lat: double.parse(message['lat'] as String),
@ -296,7 +295,7 @@ class _HomePageState extends State<HomePage> {
// if our store is loaded, we just dispatch the notification, if not, we wait til is loaded // if our store is loaded, we just dispatch the notification, if not, we wait til is loaded
if (notif != null) { if (notif != null) {
if (isLoaded) { if (isLoaded) {
store.dispatch(new AddFireNotificationAction(notif)); store.dispatch(AddFireNotificationAction(notif));
} else { } else {
newNotifications.add(notif); newNotifications.add(notif);
} }

View file

@ -1,13 +1,18 @@
import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'homePage.dart';
import 'generated/i18n.dart'; import 'generated/i18n.dart';
import 'homePage.dart';
class IntroPage extends AppIntroPage { class IntroPage extends AppIntroPage {
IntroPage({super.key})
: super(
items: _fireItems,
onIntroFinish: (BuildContext context) =>
Navigator.pushNamed(context, HomePage.routeName));
static const String routeName = '/intro'; static const String routeName = '/intro';
static final fireItems = (BuildContext context) => <AppIntroItem>[ static final _fireItems = (BuildContext context) => <AppIntroItem>[
AppIntroItem( AppIntroItem(
icon: Icons.location_on, title: S.of(context).chooseAPlace), icon: Icons.location_on, title: S.of(context).chooseAPlace),
AppIntroItem( AppIntroItem(
@ -20,11 +25,4 @@ class IntroPage extends AppIntroPage {
icon: Icons.notifications_active, icon: Icons.notifications_active,
title: S.of(context).alertWhenThereIsAFire) title: S.of(context).alertWhenThereIsAFire)
]; ];
IntroPage({Key? key})
: super(
items: fireItems,
onIntroFinish: (context) =>
Navigator.pushNamed(context, HomePage.routeName),
key: key);
} }

View file

@ -4,23 +4,24 @@ import 'package:get_it/get_it.dart';
import 'package:redux/redux.dart'; import 'package:redux/redux.dart';
import 'models/appState.dart'; import 'models/appState.dart';
import 'models/fireMapState.dart';
import 'redux/fireMapActions.dart'; import 'redux/fireMapActions.dart';
/// Layer selector widget for changing map layers /// Layer selector widget for changing map layers
class LayerSelectorMapPluginWidget extends StatelessWidget { class LayerSelectorMapPluginWidget extends StatelessWidget {
const LayerSelectorMapPluginWidget({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Store<AppState> store = GetIt.instance<Store<AppState>>(); final Store<AppState> store = GetIt.instance<Store<AppState>>();
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) => builder: (BuildContext context, BoxConstraints constraints) =>
Stack(fit: StackFit.expand, children: <Widget>[ Stack(fit: StackFit.expand, children: <Widget>[
Positioned( Positioned(
top: constraints.maxHeight - 60, top: constraints.maxHeight - 60,
left: 10.0, left: 10.0,
child: new CenteredRow( child: CenteredRow(
children: <Widget>[ children: <Widget>[
new Column( Column(
children: <Widget>[_LayerSelectorButton(store)], children: <Widget>[_LayerSelectorButton(store)],
) )
], ],
@ -30,7 +31,7 @@ class LayerSelectorMapPluginWidget extends StatelessWidget {
} }
Widget _LayerSelectorButton(Store<AppState> store) { Widget _LayerSelectorButton(Store<AppState> store) {
final key = GlobalKey<PopupMenuButtonState<FireMapLayer>>(); final GlobalKey<PopupMenuButtonState<FireMapLayer>> key = GlobalKey<PopupMenuButtonState<FireMapLayer>>();
return PopupMenuButton<FireMapLayer>( return PopupMenuButton<FireMapLayer>(
key: key, key: key,
@ -40,17 +41,17 @@ class LayerSelectorMapPluginWidget extends StatelessWidget {
}, },
itemBuilder: (BuildContext context) { itemBuilder: (BuildContext context) {
return FireMapLayer.values.map((FireMapLayer layer) { return FireMapLayer.values.map((FireMapLayer layer) {
final isSelected = store.state.fireMapState.layer == layer; final bool isSelected = store.state.fireMapState.layer == layer;
return PopupMenuItem<FireMapLayer>( return PopupMenuItem<FireMapLayer>(
value: layer, value: layer,
child: Row( child: Row(
children: [ children: <Widget>[
Icon( Icon(
isSelected ? Icons.check : Icons.check, isSelected ? Icons.check : Icons.check,
color: isSelected ? Colors.blue : Colors.transparent, color: isSelected ? Colors.blue : Colors.transparent,
size: 20, size: 20,
), ),
SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
_getLayerName(layer), _getLayerName(layer),
style: TextStyle( style: TextStyle(
@ -66,7 +67,7 @@ class LayerSelectorMapPluginWidget extends StatelessWidget {
child: FloatingActionButton( child: FloatingActionButton(
backgroundColor: Colors.black26, backgroundColor: Colors.black26,
mini: true, mini: true,
child: Icon(Icons.layers), child: const Icon(Icons.layers),
onPressed: () { onPressed: () {
key.currentState?.showButtonMenu(); key.currentState?.showButtonMenu();
}, },

View file

@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:fires_flutter/models/yourLocation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:geocoding/geocoding.dart' as geo; import 'package:geocoding/geocoding.dart' as geo;
@ -8,42 +7,43 @@ import 'package:location/location.dart';
import 'package:objectid/objectid.dart'; import 'package:objectid/objectid.dart';
import 'generated/i18n.dart'; import 'generated/i18n.dart';
import 'models/yourLocation.dart';
Future<YourLocation> getUserLocation( Future<YourLocation> getUserLocation(
GlobalKey<ScaffoldState> scaffoldKey) async { GlobalKey<ScaffoldState> scaffoldKey) async {
// Platform messages may fail, so we use a try/catch PlatformException. // Platform messages may fail, so we use a try/catch PlatformException.
try { try {
Location _location = new Location(); final Location location0 = Location();
LocationData location = await _location.getLocation(); final LocationData location = await location0.getLocation();
// It seems that the lib fails with lat/lon values // It seems that the lib fails with lat/lon values
var yl = new YourLocation( final YourLocation yl = YourLocation(
id: ObjectId(), lat: location.latitude!, lon: location.longitude!); id: ObjectId(), lat: location.latitude!, lon: location.longitude!);
var address; String address;
try { try {
address = await getReverseLocation(lat: yl.lat, lon: yl.lon); address = await getReverseLocation(lat: yl.lat, lon: yl.lon);
yl.description = address as String; yl.description = address;
} catch (e) { } catch (e) {
try { try {
address = address =
await getReverseLocation(lat: yl.lat, lon: yl.lon, external: true); await getReverseLocation(lat: yl.lat, lon: yl.lon, external: true);
yl.description = address as String; yl.description = address;
} catch (e) { } catch (e) {
print('We cannot reverse geolocate'); print('We cannot reverse geolocate');
} }
} }
return yl; return yl;
} on PlatformException catch (e) { } on PlatformException catch (e) {
final context = scaffoldKey.currentContext; final BuildContext? context = scaffoldKey.currentContext;
if (context != null) { if (context != null) {
if (e.code == 'PERMISSION_DENIED') { if (e.code == 'PERMISSION_DENIED') {
ScaffoldMessenger.of(context).showSnackBar(new SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: new Text(S.of(context).notPermsUbication), content: Text(S.of(context).notPermsUbication),
)); ));
} else if (e.code == 'PERMISSION_DENIED_NEVER_ASK') {} } else if (e.code == 'PERMISSION_DENIED_NEVER_ASK') {}
ScaffoldMessenger.of(context).showSnackBar(new SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: new Text(S.of(context).isYourUbicationEnabled), content: Text(S.of(context).isYourUbicationEnabled),
)); ));
} }
return YourLocation.noLocation; return YourLocation.noLocation;
@ -53,19 +53,19 @@ Future<YourLocation> getUserLocation(
Future<String> getReverseLocation( Future<String> getReverseLocation(
{required double lat, required double lon, bool external = false}) async { {required double lat, required double lon, bool external = false}) async {
try { try {
List<geo.Placemark> placemarks = final List<geo.Placemark> placemarks =
await geo.placemarkFromCoordinates(lat, lon); await geo.placemarkFromCoordinates(lat, lon);
if (placemarks.isNotEmpty) { if (placemarks.isNotEmpty) {
var first = placemarks.first; final geo.Placemark first = placemarks.first;
String address = final String address =
'${first.street}, ${first.locality}, ${first.administrativeArea}, ${first.country}'; '${first.street}, ${first.locality}, ${first.administrativeArea}, ${first.country}';
print("${first.name} : $address"); print('${first.name} : $address');
return address; return address;
} else { } else {
return 'Unable to determine address'; return 'Unable to determine address';
} }
} catch (e) { } catch (e) {
print("Error in reverse geocoding: $e"); print('Error in reverse geocoding: $e');
throw Exception("Failed to reverse geocode: $e"); throw Exception('Failed to reverse geocode: $e');
} }
} }

View file

@ -16,12 +16,12 @@ import 'redux/reducers.dart';
import 'sentryReport.dart'; import 'sentryReport.dart';
Future<PackageInfo> loadPackageInfo() async { Future<PackageInfo> loadPackageInfo() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform(); final PackageInfo packageInfo = await PackageInfo.fromPlatform();
return packageInfo; return packageInfo;
} }
Future<Map<String, dynamic>> loadSecrets() async { Future<Map<String, dynamic>> loadSecrets() async {
return await SecretLoader( return SecretLoader(
secretPath: globals.isDevelopment secretPath: globals.isDevelopment
? 'assets/private-settings-dev.json' ? 'assets/private-settings-dev.json'
: 'assets/private-settings.json') : 'assets/private-settings.json')
@ -34,29 +34,29 @@ Future<void> mainCommon(List<Middleware<AppState>> otherMiddleware) async {
// Initialize Firebase before any Firebase-dependent code runs // Initialize Firebase before any Firebase-dependent code runs
await Firebase.initializeApp(); await Firebase.initializeApp();
final getIt = GetIt.instance; final GetIt getIt = GetIt.instance;
getIt.registerSingleton<FiresApi>(FiresApi()); getIt.registerSingleton<FiresApi>(FiresApi());
loadPackageInfo().then((packageInfo) { loadPackageInfo().then((PackageInfo packageInfo) {
globals.appVersion = packageInfo.version; globals.appVersion = packageInfo.version;
print('Running version ${packageInfo.version}'); print('Running version ${packageInfo.version}');
loadSecrets().then((secrets) { loadSecrets().then((Map<String, dynamic> secrets) {
final store = Store<AppState>(appStateReducer, final Store<AppState> store = Store<AppState>(appStateReducer,
initialState: AppState( initialState: AppState(
gmapKey: secrets['gmapKey'] as String, gmapKey: secrets['gmapKey'] as String,
firesApiKey: secrets['firesApiKey'] as String, firesApiKey: secrets['firesApiKey'] as String,
serverUrl: secrets['firesApiUrl'] as String, serverUrl: secrets['firesApiUrl'] as String,
firesApiUrl: (secrets['firesApiUrl'] as String) + "api/v1/"), firesApiUrl: "${secrets['firesApiUrl'] as String}api/v1/"),
middleware: List.from(otherMiddleware)..add(fetchDataMiddleware)); middleware: List.from(otherMiddleware)..add(fetchDataMiddleware));
getIt.registerSingleton<Store<AppState>>(store); getIt.registerSingleton<Store<AppState>>(store);
getIt.registerSingleton<String>(store.state.firesApiUrl, getIt.registerSingleton<String>(store.state.firesApiUrl,
instanceName: "firesApiUrl"); instanceName: 'firesApiUrl');
getIt.registerSingleton<String>(store.state.firesApiKey, getIt.registerSingleton<String>(store.state.firesApiKey,
instanceName: "firesApiKey"); instanceName: 'firesApiKey');
getIt.registerSingleton<String>(store.state.serverUrl, getIt.registerSingleton<String>(store.state.serverUrl,
instanceName: "serverUrl"); instanceName: 'serverUrl');
getIt.registerSingleton<String>(store.state.gmapKey, getIt.registerSingleton<String>(store.state.gmapKey,
instanceName: "gmapKey"); instanceName: 'gmapKey');
// https://flutter.io/cookbook/maintenance/error-reporting/ // https://flutter.io/cookbook/maintenance/error-reporting/
runZonedGuarded<Future<void>>(() async { runZonedGuarded<Future<void>>(() async {
@ -68,7 +68,7 @@ Future<void> mainCommon(List<Middleware<AppState>> otherMiddleware) async {
}); });
// Listen to store changes, and re-render when the state is updated // Listen to store changes, and re-render when the state is updated
store.onChange.listen((state) { store.onChange.listen((AppState state) {
// print('Store onChange'); // print('Store onChange');
}); });
FlutterError.onError = (FlutterErrorDetails details) { FlutterError.onError = (FlutterErrorDetails details) {

View file

@ -8,7 +8,7 @@ enum LogLevel { none, actions, all }
void main() async { void main() async {
globals.isDevelopment = true; globals.isDevelopment = true;
debugPrint("Is development!"); debugPrint('Is development!');
// Simple logging middleware para desarrollo // Simple logging middleware para desarrollo
Middleware<dynamic> createLoggingMiddleware() { Middleware<dynamic> createLoggingMiddleware() {
@ -18,10 +18,10 @@ void main() async {
}; };
} }
LogLevel logRedux = LogLevel.actions; const LogLevel logRedux = LogLevel.actions;
List<Middleware<dynamic>> devMiddlewares = final List<Middleware<dynamic>> devMiddlewares =
logRedux == LogLevel.actions ? [] : [createLoggingMiddleware()]; logRedux == LogLevel.actions ? <Middleware<dynamic>>[] : <Middleware<dynamic>>[createLoggingMiddleware()];
// In development, Sentry is disabled, so we skip initialization // In development, Sentry is disabled, so we skip initialization
await mainCommon(devMiddlewares); await mainCommon(devMiddlewares);

View file

@ -1,7 +1,7 @@
import 'package:badges/badges.dart' as badges_pkg; import 'package:badges/badges.dart' as badges_pkg;
import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/src/store.dart';
import 'activeFires.dart'; import 'activeFires.dart';
import 'colors.dart'; import 'colors.dart';
@ -17,11 +17,10 @@ import 'supportPage.dart';
@immutable @immutable
class _ViewModel { class _ViewModel {
final int unreadCount; const _ViewModel({
_ViewModel({
required this.unreadCount, required this.unreadCount,
}); });
final int unreadCount;
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
@ -36,65 +35,65 @@ class _ViewModel {
} }
class MainDrawer extends Drawer { class MainDrawer extends Drawer {
MainDrawer(BuildContext context, String currentRoute, {Key? key}) MainDrawer(BuildContext context, String currentRoute, {super.key})
: super(key: key, child: mainDrawer(context, currentRoute)); : super(child: mainDrawer(context, currentRoute));
} }
Widget mainDrawer(BuildContext context, String currentRoute) { Widget mainDrawer(BuildContext context, String currentRoute) {
return new StoreConnector<AppState, _ViewModel>( return StoreConnector<AppState, _ViewModel>(
distinct: true, distinct: true,
converter: (store) { converter: (Store<AppState> store) {
return new _ViewModel(unreadCount: store.state.fireNotificationsUnread); return _ViewModel(unreadCount: store.state.fireNotificationsUnread);
}, },
builder: (context, view) { builder: (BuildContext context, _ViewModel view) {
final bottomTextStyle = const TextStyle bottomTextStyle =
new TextStyle(fontSize: 12.0, color: Colors.grey); TextStyle(fontSize: 12.0, color: Colors.grey);
return new ListView( return ListView(
// Important: Remove any padding from the ListView. // Important: Remove any padding from the ListView.
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
children: (<Widget?>[ children: <Widget?>[
new GestureDetector( GestureDetector(
onTap: () { onTap: () {
Navigator.popAndPushNamed(context, '/'); Navigator.popAndPushNamed(context, '/');
}, },
child: new DrawerHeader( child: DrawerHeader(
child: new Column( decoration: const BoxDecoration(
color: fires300,
),
child: Column(
children: <Widget>[ children: <Widget>[
new Image.asset( Image.asset(
'images/logo-200.png', 'images/logo-200.png',
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
height: 80.0, height: 80.0,
), ),
const SizedBox(height: 20.0), const SizedBox(height: 20.0),
new Text(S.of(context).appName, Text(S.of(context).appName,
style: new TextStyle( style: const TextStyle(
fontSize: 24.0, fontSize: 24.0,
color: Colors.white, color: Colors.white,
)), )),
], ],
), ),
decoration: new BoxDecoration(
color: fires300,
),
), ),
), ),
new ListTile( ListTile(
leading: const Icon(Icons.whatshot), leading: const Icon(Icons.whatshot),
title: new Text(S.of(context).activeFires), title: Text(S.of(context).activeFires),
selected: currentRoute == ActiveFiresPage.routeName, selected: currentRoute == ActiveFiresPage.routeName,
onTap: () { onTap: () {
Navigator.popAndPushNamed(context, ActiveFiresPage.routeName); Navigator.popAndPushNamed(context, ActiveFiresPage.routeName);
}, },
), ),
new ListTile( ListTile(
leading: const Icon(Icons.notifications_active), leading: const Icon(Icons.notifications_active),
selected: currentRoute == FireAlert.routeName, selected: currentRoute == FireAlert.routeName,
title: new Text(S.of(context).notifyAFire), title: Text(S.of(context).notifyAFire),
onTap: () { onTap: () {
Navigator.popAndPushNamed(context, FireAlert.routeName); Navigator.popAndPushNamed(context, FireAlert.routeName);
}, },
), ),
new ListTile( ListTile(
leading: const Icon(Icons.notifications), leading: const Icon(Icons.notifications),
selected: currentRoute == FireNotificationList.routeName, selected: currentRoute == FireNotificationList.routeName,
title: Text(S.of(context).fireNotificationsTitleShort), title: Text(S.of(context).fireNotificationsTitleShort),
@ -107,10 +106,10 @@ Widget mainDrawer(BuildContext context, String currentRoute) {
position: badges_pkg.BadgePosition.topEnd( position: badges_pkg.BadgePosition.topEnd(
top: -10, end: -12), top: -10, end: -12),
badgeContent: Text( badgeContent: Text(
'${view.unreadCount.toString()}', view.unreadCount.toString(),
style: TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
), ),
child: Icon(Icons.notifications)) child: const Icon(Icons.notifications))
])), ])),
// Text(S.of(context).fireNotificationsTitleShort), // Text(S.of(context).fireNotificationsTitleShort),
@ -119,43 +118,44 @@ Widget mainDrawer(BuildContext context, String currentRoute) {
context, FireNotificationList.routeName); context, FireNotificationList.routeName);
}, },
), ),
new ListTile( ListTile(
leading: const Icon(Icons.map), leading: const Icon(Icons.map),
selected: currentRoute == MonitoredAreasPage.routeName, selected: currentRoute == MonitoredAreasPage.routeName,
title: new Text(S.of(context).monitoredAreasTitle), title: Text(S.of(context).monitoredAreasTitle),
onTap: () { onTap: () {
Navigator.popAndPushNamed( Navigator.popAndPushNamed(
context, MonitoredAreasPage.routeName); context, MonitoredAreasPage.routeName);
}, },
), ),
new Divider(), const Divider(),
new ListTile( ListTile(
leading: const Icon(Icons.favorite), leading: const Icon(Icons.favorite),
selected: currentRoute == SupportPage.routeName, selected: currentRoute == SupportPage.routeName,
title: new Text(S.of(context).supportThisInitiative), title: Text(S.of(context).supportThisInitiative),
onTap: () { onTap: () {
Navigator.popAndPushNamed(context, SupportPage.routeName); Navigator.popAndPushNamed(context, SupportPage.routeName);
}, },
), ),
new ListTile( ListTile(
leading: const Icon(Icons.lock), leading: const Icon(Icons.lock),
selected: currentRoute == PrivacyPage.routeName, selected: currentRoute == PrivacyPage.routeName,
title: new Text(S.of(context).privacyPolicy), title: Text(S.of(context).privacyPolicy),
onTap: () { onTap: () {
Navigator.popAndPushNamed(context, PrivacyPage.routeName); Navigator.popAndPushNamed(context, PrivacyPage.routeName);
}, },
), ),
globals.isDevelopment if (globals.isDevelopment)
? new ListTile( ListTile(
leading: const Icon(Icons.bug_report), leading: const Icon(Icons.bug_report),
title: new Text('Sandbox'), title: const Text('Sandbox'),
selected: currentRoute == Sandbox.routeName, selected: currentRoute == Sandbox.routeName,
onTap: () { onTap: () {
Navigator.popAndPushNamed(context, Sandbox.routeName); Navigator.popAndPushNamed(context, Sandbox.routeName);
}, },
) )
: null, else
new AboutListTile( null,
AboutListTile(
icon: globals.appIcon, icon: globals.appIcon,
applicationName: S.of(context).appName, applicationName: S.of(context).appName,
applicationVersion: globals.appVersion, applicationVersion: globals.appVersion,
@ -163,13 +163,13 @@ Widget mainDrawer(BuildContext context, String currentRoute) {
applicationLegalese: applicationLegalese:
S.of(context).appLicense(DateTime.now().year.toString()), S.of(context).appLicense(DateTime.now().year.toString()),
aboutBoxChildren: <Widget>[ aboutBoxChildren: <Widget>[
new SizedBox(height: 10.0), const SizedBox(height: 10.0),
new Text(S.of(context).appMoto), Text(S.of(context).appMoto),
// , style: new TextStyle(fontStyle: FontStyle.italic)), // , style: new TextStyle(fontStyle: FontStyle.italic)),
new SizedBox(height: 10.0), const SizedBox(height: 10.0),
new Text(S.of(context).NASAAck, style: bottomTextStyle), Text(S.of(context).NASAAck, style: bottomTextStyle),
// More ? // More ?
]) ])
]).where((w) => w != null).cast<Widget>().toList()); ].where((Widget? w) => w != null).cast<Widget>().toList());
}); });
} }

View file

@ -1,25 +1,27 @@
import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:redux/src/store.dart';
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/sentry_flutter.dart';
import 'globals.dart' as globals; import 'globals.dart' as globals;
import 'mainCommon.dart'; import 'mainCommon.dart';
import 'models/appState.dart';
Future<void> main() async { Future<void> main() async {
globals.isDevelopment = false; globals.isDevelopment = false;
// Load secrets to get Sentry DSN // Load secrets to get Sentry DSN
final secrets = await loadSecrets(); final Map<String, dynamic> secrets = await loadSecrets();
await SentryFlutter.init( await SentryFlutter.init(
(options) { (SentryFlutterOptions options) {
options.dsn = secrets['sentryDSN'] as String?; options.dsn = secrets['sentryDSN'] as String?;
}, },
appRunner: () => mainCommon([]), appRunner: () => mainCommon(<Middleware<AppState>>[]),
); );
} }
Future<Map<String, dynamic>> loadSecrets() async { Future<Map<String, dynamic>> loadSecrets() async {
return await SecretLoader( return SecretLoader(
secretPath: globals.isDevelopment secretPath: globals.isDevelopment
? 'assets/private-settings-dev.json' ? 'assets/private-settings-dev.json'
: 'assets/private-settings.json') : 'assets/private-settings.json')

View file

@ -8,40 +8,40 @@ import 'globals.dart' as globals;
import 'mainDrawer.dart'; import 'mainDrawer.dart';
abstract class MarkdownPage extends StatefulWidget { abstract class MarkdownPage extends StatefulWidget {
const MarkdownPage({super.key, required this.title, required this.route, required this.file});
final String title; final String title;
final Future<String> file; final Future<String> file;
final String route; final String route;
MarkdownPage({required this.title, required this.route, required this.file});
@override @override
_MarkdownPageState createState() => _MarkdownPageState createState() =>
_MarkdownPageState(title: this.title, file: this.file, route: this.route); _MarkdownPageState(title: title, file: file, route: route);
} }
class _MarkdownPageState extends State<MarkdownPage> { class _MarkdownPageState extends State<MarkdownPage> {
final String title;
final String route;
final Future<String> file;
String pageData = "";
_MarkdownPageState( _MarkdownPageState(
{required this.title, required this.file, required this.route}); {required this.title, required this.file, required this.route});
final String title;
final String route;
final Future<String> file;
String pageData = '';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
this.file.then((fileSync) { file.then((String fileSync) {
rootBundle rootBundle
.loadString(fileSync, cache: !globals.isDevelopment) .loadString(fileSync, cache: !globals.isDevelopment)
.then((pageData) { .then((String pageData) {
setState(() { setState(() {
this.pageData = pageData; this.pageData = pageData;
}); });
}); });
}); });
return new Scaffold( return Scaffold(
appBar: new AppBar(title: new Text(title ?? '')), appBar: AppBar(title: Text(title ?? '')),
drawer: new MainDrawer(context, route), drawer: MainDrawer(context, route),
body: new Markdown(data: pageData)); body: Markdown(data: pageData));
} }
} }

View file

@ -1,8 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:fires_flutter/models/fireNotification.dart';
import 'package:fires_flutter/models/yourLocation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
@ -10,7 +8,9 @@ import 'package:latlong2/latlong.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'fireMapState.dart'; import 'fireMapState.dart';
import 'fireNotification.dart';
import 'user.dart'; import 'user.dart';
import 'yourLocation.dart';
export 'fireMapState.dart'; export 'fireMapState.dart';
@ -19,6 +19,25 @@ part 'appState.g.dart';
@immutable @immutable
@JsonSerializable(nullable: false) @JsonSerializable(nullable: false)
class AppState { class AppState {
const AppState(
{this.yourLocations = const <YourLocation>[],
this.fireNotifications = const <FireNotification>[],
this.fireNotificationsUnread = 0,
this.user = const User.initial(),
this.isLoading = false,
this.isLoaded = false,
this.error = '',
this.gmapKey = '',
this.firesApiKey = '',
this.firesApiUrl = '',
this.serverUrl = '',
this.monitoredAreas = const <Polyline>[],
this.fireMapState = const FireMapState.initial()});
@JsonKey(ignore: true)
factory AppState.fromJson(Map<String, dynamic> json) =>
_$AppStateFromJson(json);
@JsonKey(ignore: true) @JsonKey(ignore: true)
final bool isLoading; final bool isLoading;
@JsonKey(ignore: true) @JsonKey(ignore: true)
@ -44,25 +63,6 @@ class AppState {
@JsonKey(ignore: true) @JsonKey(ignore: true)
final FireMapState fireMapState; final FireMapState fireMapState;
@JsonKey(ignore: true)
factory AppState.fromJson(Map<String, dynamic> json) =>
_$AppStateFromJson(json);
AppState(
{this.yourLocations = const <YourLocation>[],
this.fireNotifications = const <FireNotification>[],
this.fireNotificationsUnread = 0,
this.user = const User.initial(),
this.isLoading = false,
this.isLoaded = false,
this.error = '',
this.gmapKey = '',
this.firesApiKey = '',
this.firesApiUrl = '',
this.serverUrl = '',
this.monitoredAreas = const <Polyline>[],
this.fireMapState = const FireMapState.initial()});
AppState copyWith( AppState copyWith(
{bool? isLoading, {bool? isLoading,
bool? isLoaded, bool? isLoaded,
@ -77,7 +77,7 @@ class AppState {
int? fireNotificationsUnread, int? fireNotificationsUnread,
FireMapState? fireMapState, FireMapState? fireMapState,
List<Polyline>? monitoredAreas}) { List<Polyline>? monitoredAreas}) {
return new AppState( return AppState(
isLoading: isLoading ?? this.isLoading, isLoading: isLoading ?? this.isLoading,
isLoaded: isLoaded ?? this.isLoaded, isLoaded: isLoaded ?? this.isLoaded,
user: user ?? this.user, user: user ?? this.user,
@ -100,24 +100,24 @@ class AppState {
} }
} }
typedef void AddYourLocationFunction(YourLocation loc); typedef AddYourLocationFunction = void Function(YourLocation loc);
typedef void DeleteYourLocationFunction(YourLocation loc); typedef DeleteYourLocationFunction = void Function(YourLocation loc);
typedef void OnRefreshYourLocationsFunction(Completer<Null> callback); typedef OnRefreshYourLocationsFunction = void Function(Completer<void> callback);
typedef void ToggleSubscriptionFunction(YourLocation loc); typedef ToggleSubscriptionFunction = void Function(YourLocation loc);
typedef void OnLocationTapFunction(YourLocation loc); typedef OnLocationTapFunction = void Function(YourLocation loc);
typedef void OnSubscribeFunction(YourLocation loc); typedef OnSubscribeFunction = void Function(YourLocation loc);
typedef void OnSubscribeDistanceChangeFunction(YourLocation loc); typedef OnSubscribeDistanceChangeFunction = void Function(YourLocation loc);
typedef void OnUnSubscribeFunction(YourLocation loc); typedef OnUnSubscribeFunction = void Function(YourLocation loc);
typedef void OnSubscribeConfirmedFunction(YourLocation loc); typedef OnSubscribeConfirmedFunction = void Function(YourLocation loc);
typedef void OnLocationEdit(YourLocation loc); typedef OnLocationEdit = void Function(YourLocation loc);
typedef void OnLocationEditing(YourLocation loc); typedef OnLocationEditing = void Function(YourLocation loc);
typedef void OnLocationEditConfirm(YourLocation loc); typedef OnLocationEditConfirm = void Function(YourLocation loc);
typedef void OnLocationEditCancel(YourLocation loc); typedef OnLocationEditCancel = void Function(YourLocation loc);
typedef void TapFireNotificationFunction(FireNotification notif); typedef TapFireNotificationFunction = void Function(FireNotification notif);
typedef void OnFirePressedInMap(LatLng latLng, DateTime when, String source); typedef OnFirePressedInMap = void Function(LatLng latLng, DateTime when, String source);
// typedef void OnReceivedFireNotificationFunction(FireNotification notif); // typedef void OnReceivedFireNotificationFunction(FireNotification notif);
typedef void DeleteFireNotificationFunction(FireNotification notif); typedef DeleteFireNotificationFunction = void Function(FireNotification notif);
typedef void DeleteAllFireNotificationFunction(); typedef DeleteAllFireNotificationFunction = void Function();
// unused // unused
// typedef void UpdateYourLocationFunction(ObjectId id, YourLocation loc); // typedef void UpdateYourLocationFunction(ObjectId id, YourLocation loc);

View file

@ -1,18 +1,26 @@
class BasicLocation implements Comparable<BasicLocation> { class BasicLocation implements Comparable<BasicLocation> {
// static BasicLocation noLocation = new BasicLocation(lat: 0.0, lon: 0.0);
BasicLocation({required this.lat, required this.lon, this.description});
BasicLocation.fromJson(Map<String, dynamic> json)
: lat = (json['lat'] as num).toDouble(),
lon = (json['lon'] as num).toDouble(),
description = json['description'] as String?;
final double lat; final double lat;
final double lon; final double lon;
final String? description; final String? description;
// static BasicLocation noLocation = new BasicLocation(lat: 0.0, lon: 0.0); @override
BasicLocation({required this.lat, required this.lon, this.description}) {}
int compareTo(BasicLocation other) { int compareTo(BasicLocation other) {
return lat == other.lat && lon == other.lon ? 1 : 0; return lat == other.lat && lon == other.lon ? 1 : 0;
} }
bool operator ==(o) => o is BasicLocation && o.lat == lat && o.lon == lon; @override
bool operator ==(Object o) => o is BasicLocation && o.lat == lat && o.lon == lon;
@override
int get hashCode { int get hashCode {
int hash = 1; int hash = 1;
hash = hash * 17 + lat.hashCode; hash = hash * 17 + lat.hashCode;
@ -20,12 +28,7 @@ class BasicLocation implements Comparable<BasicLocation> {
return hash; return hash;
} }
BasicLocation.fromJson(Map<String, dynamic> json) Map<String, dynamic> toJson() => <String, dynamic>{
: lat = (json['lat'] as num).toDouble(),
lon = (json['lon'] as num).toDouble(),
description = json['description'] as String?;
Map<String, dynamic> toJson() => {
'lat': lat, 'lat': lat,
'lon': lon, 'lon': lon,
'description': description, 'description': description,

View file

@ -1,7 +1,8 @@
import 'package:fires_flutter/models/fireNotification.dart';
import 'package:fires_flutter/models/yourLocation.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'fireNotification.dart';
import 'yourLocation.dart';
enum FireMapStatus { enum FireMapStatus {
view, view,
subscriptionConfirm, subscriptionConfirm,
@ -14,6 +15,26 @@ enum FireMapLayer { osmcGrey, esriSatellite, osmc, esri, esriTerrain }
@immutable @immutable
class FireMapState { class FireMapState {
const FireMapState(
{this.status = FireMapStatus.view,
this.layer = FireMapLayer.osmcGrey,
this.yourLocation,
this.numFires = 0,
this.fires = const <dynamic>[],
this.fireNotification,
this.falsePos = const <dynamic>[],
this.industries = const <dynamic>[]});
const FireMapState.initial()
: status = FireMapStatus.view,
layer = FireMapLayer.osmcGrey,
yourLocation = null,
fireNotification = null,
numFires = 0,
fires = const <dynamic>[],
falsePos = const <dynamic>[],
industries = const <dynamic>[];
final FireMapStatus status; final FireMapStatus status;
final FireMapLayer layer; final FireMapLayer layer;
final int numFires; final int numFires;
@ -23,26 +44,6 @@ class FireMapState {
final List<dynamic> industries; final List<dynamic> industries;
final YourLocation? yourLocation; final YourLocation? yourLocation;
const FireMapState.initial()
: this.status = FireMapStatus.view,
this.layer = FireMapLayer.osmcGrey,
this.yourLocation = null,
this.fireNotification = null,
this.numFires = 0,
this.fires = const [],
this.falsePos = const [],
this.industries = const [];
FireMapState(
{this.status = FireMapStatus.view,
this.layer = FireMapLayer.osmcGrey,
this.yourLocation,
this.numFires = 0,
this.fires = const [],
this.fireNotification,
this.falsePos = const [],
this.industries = const []});
FireMapState copyWith({ FireMapState copyWith({
FireMapStatus? status, FireMapStatus? status,
FireMapLayer? layer, FireMapLayer? layer,
@ -53,9 +54,9 @@ class FireMapState {
List<dynamic>? falsePos, List<dynamic>? falsePos,
List<dynamic>? industries, List<dynamic>? industries,
}) { }) {
return new FireMapState( return FireMapState(
yourLocation: yourLocation ?? this.yourLocation, yourLocation: yourLocation ?? this.yourLocation,
fireNotification: fireNotication ?? this.fireNotification, fireNotification: fireNotication ?? fireNotification,
numFires: numFires ?? this.numFires, numFires: numFires ?? this.numFires,
fires: fires ?? this.fires, fires: fires ?? this.fires,
falsePos: falsePos ?? this.falsePos, falsePos: falsePos ?? this.falsePos,

View file

@ -9,6 +9,19 @@ part 'fireNotification.g.dart';
@JsonSerializable(nullable: false) @JsonSerializable(nullable: false)
class FireNotification { class FireNotification {
FireNotification(
{required this.id,
required this.lat,
required this.lon,
required this.description,
required this.when,
required this.read,
required this.sealed,
required this.subsId});
factory FireNotification.fromJson(Map<String, dynamic> json) =>
_$FireNotificationFromJson(json);
@JsonKey(toJson: objectIdToJson, fromJson: objectIdFromJson) @JsonKey(toJson: objectIdToJson, fromJson: objectIdFromJson)
ObjectId id; ObjectId id;
final double lat; final double lat;
@ -20,19 +33,6 @@ class FireNotification {
final ObjectId subsId; final ObjectId subsId;
final bool read; final bool read;
factory FireNotification.fromJson(Map<String, dynamic> json) =>
_$FireNotificationFromJson(json);
FireNotification(
{required this.id,
required this.lat,
required this.lon,
required this.description,
required this.when,
required this.read,
required this.sealed,
required this.subsId}) {}
FireNotification copyWith( FireNotification copyWith(
{ObjectId? id, {ObjectId? id,
double? lat, double? lat,
@ -42,7 +42,7 @@ class FireNotification {
DateTime? when, DateTime? when,
String? sealed, String? sealed,
ObjectId? subsId}) { ObjectId? subsId}) {
return new FireNotification( return FireNotification(
id: id ?? this.id, id: id ?? this.id,
lat: lat ?? this.lat, lat: lat ?? this.lat,
lon: lon ?? this.lon, lon: lon ?? this.lon,
@ -83,7 +83,7 @@ class FireNotification {
return 'FireNotification {id: $id, lat: $lat, lon: $lon, when: $when, read: $read, subsId: $subsId, sealed ${ellipse(sealed)}'; return 'FireNotification {id: $id, lat: $lat, lon: $lon, when: $when, read: $read, subsId: $subsId, sealed ${ellipse(sealed)}';
} }
static final Map<String, Route<Null>> routes = <String, Route<Null>>{}; static final Map<String, Route<void>> routes = <String, Route<void>>{};
Map<String, dynamic> toJson() => _$FireNotificationToJson(this); Map<String, dynamic> toJson() => _$FireNotificationToJson(this);
/* /*

View file

@ -2,30 +2,31 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:fires_flutter/models/fireNotification.dart'; import 'package:shared_preferences/src/shared_preferences_legacy.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
import 'fireNotification.dart';
final String fireNotificationKey = 'fireNotifications'; const String fireNotificationKey = 'fireNotifications';
Future<List<FireNotification>> loadFireNotifications() async { Future<List<FireNotification>> loadFireNotifications() async {
return await globals.prefs.then((prefs) { return globals.prefs.then((SharedPreferences prefs) {
List<String>? FireNotifications = prefs.getStringList(fireNotificationKey); final List<String>? FireNotifications = prefs.getStringList(fireNotificationKey);
List<FireNotification> persistedList = []; final List<FireNotification> persistedList = <FireNotification>[];
(FireNotifications ?? []).forEach((notificationString) { for (final String notificationString in (FireNotifications ?? <String>[])) {
Map<String, dynamic> notificationMap = final Map<String, dynamic> notificationMap =
json.decode(notificationString) as Map<String, dynamic>; json.decode(notificationString) as Map<String, dynamic>;
persistedList.add(FireNotification.fromJson(notificationMap)); persistedList.add(FireNotification.fromJson(notificationMap));
}); }
return persistedList; return persistedList;
}); });
} }
persistFireNotifications(List<FireNotification> notif) { void persistFireNotifications(List<FireNotification> notif) {
// print('Persisting $notif'); // print('Persisting $notif');
globals.prefs.then((prefs) { globals.prefs.then((SharedPreferences prefs) {
List<String> notifAsString = []; final List<String> notifAsString = <String>[];
notif.where(notNull).toList().forEach((notification) { notif.where(notNull).toList().forEach((FireNotification notification) {
notifAsString.add(json.encode(notification.toJson())); notifAsString.add(json.encode(notification.toJson()));
}); });
prefs.setStringList(fireNotificationKey, notifAsString); prefs.setStringList(fireNotificationKey, notifAsString);

View file

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:fires_flutter/models/yourLocation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
@ -12,49 +11,50 @@ import '../objectIdUtils.dart';
import '../redux/actions.dart'; import '../redux/actions.dart';
import 'appState.dart'; import 'appState.dart';
import 'falsePositiveTypes.dart'; import 'falsePositiveTypes.dart';
import 'yourLocation.dart';
class FiresApi { class FiresApi {
late final Dio _dio;
FiresApi() { FiresApi() {
_dio = Dio(); _dio = Dio();
} }
late final Dio _dio;
Future<String> createUser( Future<String> createUser(
AppState state, String mobileToken, String lang) async { AppState state, String mobileToken, String lang) async {
final params = { final Map<String, String> params = <String, String>{
"token": state.firesApiKey, 'token': state.firesApiKey,
"mobileToken": mobileToken, 'mobileToken': mobileToken,
"lang": lang 'lang': lang
}; };
final String url = '${state.firesApiUrl}mobile/users'; final String url = '${state.firesApiUrl}mobile/users';
try { try {
final response = await _dio.post(url, data: params); final Response<dynamic> response = await _dio.post(url, data: params);
if (response.statusCode == 200) { if (response.statusCode == 200) {
return response.data['data']['userId'] as String; return response.data['data']['userId'] as String;
} else { } else {
throw "Unexpected error on create user"; throw 'Unexpected error on create user';
} }
} catch (e) { } catch (e) {
throw "Error creating user: $e"; throw 'Error creating user: $e';
} }
} }
Future<List<YourLocation>> fetchYourLocations(AppState state) async { Future<List<YourLocation>> fetchYourLocations(AppState state) async {
final apiKey = state.firesApiKey; final String apiKey = state.firesApiKey;
final mobileToken = state.user.token; final String mobileToken = state.user.token;
final String url = final String url =
'${state.firesApiUrl}mobile/subscriptions/all/$apiKey/$mobileToken'; '${state.firesApiUrl}mobile/subscriptions/all/$apiKey/$mobileToken';
try { try {
final response = await _dio.get(url); final Response<dynamic> response = await _dio.get(url);
if (response.statusCode == 200) { if (response.statusCode == 200) {
final dataSubscriptions = final List<dynamic> dataSubscriptions =
response.data['data']['subscriptions'] as List<dynamic>; response.data['data']['subscriptions'] as List<dynamic>;
List<YourLocation> subscribed = []; final List<YourLocation> subscribed = <YourLocation>[];
for (int i = 0; i < dataSubscriptions.length; i++) { for (int i = 0; i < dataSubscriptions.length; i++) {
var el = dataSubscriptions[i] as Map<String, dynamic>; final Map<String, dynamic> el = dataSubscriptions[i] as Map<String, dynamic>;
var lat = (el['location']['lat'] as num).toDouble(); final double lat = (el['location']['lat'] as num).toDouble();
var lon = (el['location']['lon'] as num).toDouble(); final double lon = (el['location']['lon'] as num).toDouble();
subscribed.add(YourLocation( subscribed.add(YourLocation(
id: objectIdFromJson(el['_id']['_str'] as String), id: objectIdFromJson(el['_id']['_str'] as String),
lat: lat, lat: lat,
@ -64,50 +64,50 @@ class FiresApi {
} }
return subscribed; return subscribed;
} else { } else {
throw "Unexpected error fetching your locations"; throw 'Unexpected error fetching your locations';
} }
} catch (e) { } catch (e) {
throw "Error fetching locations: $e"; throw 'Error fetching locations: $e';
} }
} }
Future<String> subscribe(AppState state, YourLocation loc) async { Future<String> subscribe(AppState state, YourLocation loc) async {
final params = { final Map<String, Object> params = <String, Object>{
"token": state.firesApiKey, 'token': state.firesApiKey,
"mobileToken": state.user.token, 'mobileToken': state.user.token,
"id": loc.id.hexString, 'id': loc.id.hexString,
"lat": loc.lat, 'lat': loc.lat,
"lon": loc.lon, 'lon': loc.lon,
"distance": loc.distance 'distance': loc.distance
}; };
final String url = '${state.firesApiUrl}mobile/subscriptions'; final String url = '${state.firesApiUrl}mobile/subscriptions';
try { try {
final response = await _dio.post(url, data: params); final Response<dynamic> response = await _dio.post(url, data: params);
if (response.statusCode == 200) { if (response.statusCode == 200) {
return response.data['data']['subsId'] as String; return response.data['data']['subsId'] as String;
} else { } else {
print(response.data); print(response.data);
throw "Unexpected error on subscribe"; throw 'Unexpected error on subscribe';
} }
} catch (e) { } catch (e) {
throw "Error subscribing: $e"; throw 'Error subscribing: $e';
} }
} }
Future<bool> unsubscribe(AppState state, String subsId) async { Future<bool> unsubscribe(AppState state, String subsId) async {
final apiKey = state.firesApiKey; final String apiKey = state.firesApiKey;
final mobileToken = state.user.token; final String mobileToken = state.user.token;
final String url = final String url =
'${state.firesApiUrl}mobile/subscriptions/$apiKey/$mobileToken/$subsId'; '${state.firesApiUrl}mobile/subscriptions/$apiKey/$mobileToken/$subsId';
try { try {
final response = await _dio.delete(url); final Response<dynamic> response = await _dio.delete(url);
if (response.statusCode == 200) { if (response.statusCode == 200) {
return true; return true;
} else { } else {
throw "Unexpected error on unsubscribe"; throw 'Unexpected error on unsubscribe';
} }
} catch (e) { } catch (e) {
throw "Error unsubscribing: $e"; throw 'Error unsubscribing: $e';
} }
} }
@ -116,22 +116,22 @@ class FiresApi {
required double lat, required double lat,
required double lon, required double lon,
required int distance}) async { required int distance}) async {
var url = final String url =
'${state.firesApiUrl}fires-in-full/${state.firesApiKey}/$lat/$lon/$distance'; '${state.firesApiUrl}fires-in-full/${state.firesApiKey}/$lat/$lon/$distance';
if (globals.isDevelopment) print(url); if (globals.isDevelopment) print(url);
try { try {
final response = await _dio.get(url); final Response<dynamic> response = await _dio.get(url);
if (response.statusCode == 200) { if (response.statusCode == 200) {
var resultDecoded = response.data; final resultDecoded = response.data;
int numFires = (resultDecoded['real'] as num).toInt(); final int numFires = (resultDecoded['real'] as num).toInt();
List<dynamic> fires = resultDecoded['fires'] as List<dynamic>; final List<dynamic> fires = resultDecoded['fires'] as List<dynamic>;
List<dynamic> falsePos = resultDecoded['falsePos'] as List<dynamic>; final List<dynamic> falsePos = resultDecoded['falsePos'] as List<dynamic>;
List<dynamic> industries = resultDecoded['industries'] as List<dynamic>; final List<dynamic> industries = resultDecoded['industries'] as List<dynamic>;
if (globals.isDevelopment) { if (globals.isDevelopment) {
var firesCount = fires.length; final int firesCount = fires.length;
var industriesCount = industries.length; final int industriesCount = industries.length;
var falsePosCount = falsePos.length; final int falsePosCount = falsePos.length;
print( print(
'(Pos: $lat, $lon) real: $numFires, fire: $firesCount falsePos: $falsePosCount industries: $industriesCount'); '(Pos: $lat, $lon) real: $numFires, fire: $firesCount falsePos: $falsePosCount industries: $industriesCount');
} }
@ -148,25 +148,25 @@ class FiresApi {
} }
Future<List<Polyline>> getMonitoredAreas({required AppState state}) async { Future<List<Polyline>> getMonitoredAreas({required AppState state}) async {
var url = final String url =
'${state.firesApiUrl}status/subs-public-union/${state.firesApiKey}'; '${state.firesApiUrl}status/subs-public-union/${state.firesApiKey}';
var color = const Color(0xFF145A32); const Color color = Color(0xFF145A32);
try { try {
final response = await _dio.get(url); final Response<dynamic> response = await _dio.get(url);
if (response.statusCode == 200) { if (response.statusCode == 200) {
var resultDecoded = response.data; final resultDecoded = response.data;
List<Polyline> union = []; final List<Polyline> union = <Polyline>[];
final multipolygon = final List<dynamic> multipolygon =
(json.decode(resultDecoded['data']['union']['value'] as String) (json.decode(resultDecoded['data']['union']['value'] as String)
as Map<String, dynamic>)['geometry']['coordinates'] as Map<String, dynamic>)['geometry']['coordinates']
as List<dynamic>; as List<dynamic>;
for (dynamic polygonDynamic in multipolygon) { for (final dynamic polygonDynamic in multipolygon) {
var polygon = polygonDynamic as List<dynamic>; final List<dynamic> polygon = polygonDynamic as List<dynamic>;
for (dynamic holeDynamic in polygon) { for (final dynamic holeDynamic in polygon) {
var hole = holeDynamic as List<dynamic>; final List<dynamic> hole = holeDynamic as List<dynamic>;
List<LatLng> points = []; final List<LatLng> points = <LatLng>[];
for (dynamic pointDynamic in hole) { for (final dynamic pointDynamic in hole) {
var point = pointDynamic as List<dynamic>; final List<dynamic> point = pointDynamic as List<dynamic>;
points.add(LatLng( points.add(LatLng(
(point[1] as num).toDouble(), (point[0] as num).toDouble())); (point[1] as num).toDouble(), (point[0] as num).toDouble()));
} }
@ -183,15 +183,15 @@ class FiresApi {
Future<bool> markFalsePositive(AppState state, String mobileToken, Future<bool> markFalsePositive(AppState state, String mobileToken,
String sealed, FalsePositiveType type) async { String sealed, FalsePositiveType type) async {
final params = { final Map<String, String> params = <String, String>{
"token": state.firesApiKey, 'token': state.firesApiKey,
"mobileToken": mobileToken, 'mobileToken': mobileToken,
"sealed": sealed, 'sealed': sealed,
"type": type.toString().split('.')[1] 'type': type.toString().split('.')[1]
}; };
final String url = '${state.firesApiUrl}mobile/falsepositive'; final String url = '${state.firesApiUrl}mobile/falsepositive';
try { try {
final response = await _dio.post(url, data: params); final Response<dynamic> response = await _dio.post(url, data: params);
if (response.statusCode == 200) { if (response.statusCode == 200) {
if (globals.isDevelopment) print(response.data['data']['upsert']); if (globals.isDevelopment) print(response.data['data']['upsert']);
return true; return true;
@ -200,7 +200,7 @@ class FiresApi {
return false; return false;
} }
} catch (e) { } catch (e) {
debugPrint("Error marking false positive: $e"); debugPrint('Error marking false positive: $e');
return false; return false;
} }
} }

View file

@ -1,21 +1,21 @@
import 'package:meta/meta.dart';
import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:meta/meta.dart';
@immutable @immutable
class User { class User {
final String userId;
final String lang; const User({required this.userId, required this.lang, required this.token});
final String token;
const User.initial() const User.initial()
: userId = '', : userId = '',
lang = '', lang = '',
token = ''; token = '';
final String userId;
const User({required this.userId, required this.lang, required this.token}); final String lang;
final String token;
User copyWith({String? userId, String? lang, String? token}) { User copyWith({String? userId, String? lang, String? token}) {
return new User( return User(
userId: userId ?? this.userId, userId: userId ?? this.userId,
token: token ?? this.token, token: token ?? this.token,
lang: lang ?? this.lang); lang: lang ?? this.lang);

View file

@ -1,30 +1,12 @@
import 'package:fires_flutter/objectIdUtils.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:objectid/objectid.dart'; import 'package:objectid/objectid.dart';
import '../objectIdUtils.dart';
part 'yourLocation.g.dart'; part 'yourLocation.g.dart';
@JsonSerializable(nullable: false) @JsonSerializable(nullable: false)
class YourLocation { class YourLocation {
@JsonKey(toJson: objectIdToJson, fromJson: objectIdFromJson)
ObjectId id;
final double lat;
final double lon;
String description;
bool subscribed;
int distance;
late int currentNumFires;
factory YourLocation.fromJson(Map<String, dynamic> json) =>
_$YourLocationFromJson(json);
static late final YourLocation noLocation;
static const int? withoutStats = null;
static void _initNoLocation() {
noLocation = new YourLocation(
id: ObjectId(), lat: 0.0, lon: 0.0, description: '', subscribed: false);
}
YourLocation( YourLocation(
{required this.id, {required this.id,
@ -36,6 +18,25 @@ class YourLocation {
this.subscribed = false}) { this.subscribed = false}) {
this.currentNumFires = currentNumFires ?? 0; this.currentNumFires = currentNumFires ?? 0;
} }
factory YourLocation.fromJson(Map<String, dynamic> json) =>
_$YourLocationFromJson(json);
@JsonKey(toJson: objectIdToJson, fromJson: objectIdFromJson)
ObjectId id;
final double lat;
final double lon;
String description;
bool subscribed;
int distance;
late int currentNumFires;
static late final YourLocation noLocation;
static const int? withoutStats = null;
static void _initNoLocation() {
noLocation = YourLocation(
id: ObjectId(), lat: 0.0, lon: 0.0);
}
Map<String, dynamic> toJson() => _$YourLocationToJson(this); Map<String, dynamic> toJson() => _$YourLocationToJson(this);
YourLocation copyWith( YourLocation copyWith(
@ -46,7 +47,7 @@ class YourLocation {
int? distance, int? distance,
int? currentNumFires, int? currentNumFires,
bool? subscribed}) { bool? subscribed}) {
return new YourLocation( return YourLocation(
id: id ?? this.id, id: id ?? this.id,
lat: lat ?? this.lat, lat: lat ?? this.lat,
lon: lon ?? this.lon, lon: lon ?? this.lon,

View file

@ -2,30 +2,31 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:fires_flutter/models/yourLocation.dart'; import 'package:shared_preferences/src/shared_preferences_legacy.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
import 'yourLocation.dart';
final String locationKey = 'yourlocations'; const String locationKey = 'yourlocations';
Future<List<YourLocation>> loadYourLocations() async { Future<List<YourLocation>> loadYourLocations() async {
return await globals.prefs.then((prefs) { return globals.prefs.then((SharedPreferences prefs) {
List<String>? yourLocations = prefs.getStringList(locationKey); final List<String>? yourLocations = prefs.getStringList(locationKey);
List<YourLocation> persistedList = []; final List<YourLocation> persistedList = <YourLocation>[];
(yourLocations ?? []).forEach((locationString) { for (final String locationString in (yourLocations ?? <String>[])) {
Map<String, dynamic> locationMap = final Map<String, dynamic> locationMap =
json.decode(locationString) as Map<String, dynamic>; json.decode(locationString) as Map<String, dynamic>;
persistedList.add(YourLocation.fromJson(locationMap)); persistedList.add(YourLocation.fromJson(locationMap));
}); }
return persistedList; return persistedList;
}); });
} }
persistYourLocations(List<YourLocation> yl) { void persistYourLocations(List<YourLocation> yl) {
// debugPrint('Persisting $yl'); // debugPrint('Persisting $yl');
globals.prefs.then((prefs) { globals.prefs.then((SharedPreferences prefs) {
List<String> ylAsString = []; final List<String> ylAsString = <String>[];
yl.where(notNull).toList().forEach((location) { yl.where(notNull).toList().forEach((YourLocation location) {
ylAsString.add(json.encode(location.toJson())); ylAsString.add(json.encode(location.toJson()));
}); });
prefs.setStringList(locationKey, ylAsString); prefs.setStringList(locationKey, ylAsString);

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:redux/src/store.dart';
import 'colors.dart'; import 'colors.dart';
import 'compassMapPlugin.dart'; import 'compassMapPlugin.dart';
@ -9,12 +10,11 @@ import 'customBottomAppBar.dart';
import 'generated/i18n.dart'; import 'generated/i18n.dart';
import 'mainDrawer.dart'; import 'mainDrawer.dart';
import 'models/appState.dart'; import 'models/appState.dart';
import 'firesSpinner.dart';
class _ViewModel { class _ViewModel {
List<Polyline> monitoredAreas;
_ViewModel(this.monitoredAreas); _ViewModel(this.monitoredAreas);
List<Polyline> monitoredAreas;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@ -28,65 +28,63 @@ class _ViewModel {
} }
class MonitoredAreasPage extends StatelessWidget { class MonitoredAreasPage extends StatelessWidget {
static const String routeName = "monitoredAreasMap"; const MonitoredAreasPage({super.key});
static const String routeName = 'monitoredAreasMap';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new StoreConnector<AppState, _ViewModel>( return StoreConnector<AppState, _ViewModel>(
distinct: true, distinct: true,
converter: (store) { converter: (Store<AppState> store) {
return new _ViewModel(store.state.monitoredAreas); return _ViewModel(store.state.monitoredAreas);
}, },
builder: (context, view) { builder: (BuildContext context, _ViewModel view) {
return new Scaffold( return Scaffold(
appBar: appBar:
new AppBar(title: new Text(S.of(context).monitoredAreasTitle)), AppBar(title: Text(S.of(context).monitoredAreasTitle)),
drawer: new MainDrawer(context, MonitoredAreasPage.routeName), drawer: MainDrawer(context, MonitoredAreasPage.routeName),
bottomNavigationBar: new CustomBottomAppBar( bottomNavigationBar: CustomBottomAppBar(
fabLocation: FloatingActionButtonLocation.centerDocked, fabLocation: FloatingActionButtonLocation.centerDocked,
showNotch: true, showNotch: true,
color: fires100, color: fires100,
mainAxisAlignment: MainAxisAlignment.center,
actions: <Widget>[ actions: <Widget>[
new Flexible( Flexible(
child: new Padding( child: Padding(
padding: new EdgeInsets.only(left: 10.0, right: 10.0), padding: const EdgeInsets.only(left: 10.0, right: 10.0),
child: new Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[
children: [ Text(S.of(context).mapPrivacy,
new Text(S.of(context).mapPrivacy, style: const TextStyle(
style: new TextStyle(
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
color: Colors.black38)) color: Colors.black38))
]))) ])))
]), ]),
body: !(view.monitoredAreas is List) body: Padding(
? new FiresSpinner() padding: const EdgeInsets.all(10.0),
: new Padding( child: Column(
padding: new EdgeInsets.all(10.0), children: <Widget>[
child: new Column( Padding(
children: [ padding: const EdgeInsets.only(
new Padding( top: 8.0, bottom: 8.0),
padding: new EdgeInsets.only( child: Text(S.of(context).inGreenMonitoredAreas),
top: 8.0, bottom: 8.0, left: 0.0, right: 0.0),
child: new Text(S.of(context).inGreenMonitoredAreas),
), ),
new Flexible( Flexible(
child: new FlutterMap( child: FlutterMap(
options: new MapOptions( options: const MapOptions(
initialCenter: new LatLng(53.5775, 3.106111), initialCenter: LatLng(53.5775, 3.106111),
initialZoom: 1.0, initialZoom: 1.0,
), ),
children: [ children: <Widget>[
new TileLayer( TileLayer(
urlTemplate: urlTemplate:
"https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png", 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png',
subdomains: ['a', 'b', 'c', 'd'], subdomains: const <String>['a', 'b', 'c', 'd'],
userAgentPackageName: userAgentPackageName:
'com.example.fires_flutter'), 'com.example.fires_flutter'),
new CompassMapPluginWidget(), const CompassMapPluginWidget(),
new PolylineLayer( PolylineLayer(
polylines: view.monitoredAreas, polylines: view.monitoredAreas,
) )
], ],

View file

@ -1,10 +1,10 @@
import 'package:objectid/objectid.dart'; import 'package:objectid/objectid.dart';
ObjectId objectIdFromJson(String json) { ObjectId objectIdFromJson(String json) {
return new ObjectId.fromHexString( return ObjectId.fromHexString(
json.replaceFirst("ObjectId(", "").replaceAll(")", "")); json.replaceFirst('ObjectId(', '').replaceAll(')', ''));
} }
objectIdToJson(ObjectId o) { String objectIdToJson(ObjectId o) {
return o.toString(); return o.toString();
} }

View file

@ -1,8 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'package:fires_flutter/models/yourLocation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'models/yourLocation.dart';
/// Open a places dialog for selecting a location. /// Open a places dialog for selecting a location.
/// Currently returns a default location as the google_places_autocomplete package /// Currently returns a default location as the google_places_autocomplete package
@ -10,9 +11,9 @@ import 'package:flutter/material.dart';
/// TODO: Implement with a modern null-safe places API integration /// TODO: Implement with a modern null-safe places API integration
Future<YourLocation> openPlacesDialog(GlobalKey<ScaffoldState> sc) async { Future<YourLocation> openPlacesDialog(GlobalKey<ScaffoldState> sc) async {
// Show a snackbar informing the user that this feature is not yet available // Show a snackbar informing the user that this feature is not yet available
final messenger = ScaffoldMessenger.of(sc.currentContext!); final ScaffoldMessengerState messenger = ScaffoldMessenger.of(sc.currentContext!);
messenger.showSnackBar( messenger.showSnackBar(
SnackBar( const SnackBar(
content: Text( content: Text(
'Place selection is currently unavailable. Please use manual location entry.'), 'Place selection is currently unavailable. Please use manual location entry.'),
), ),

View file

@ -5,9 +5,8 @@ import 'generated/i18n.dart';
import 'markdownPage.dart'; import 'markdownPage.dart';
class PrivacyPage extends MarkdownPage { class PrivacyPage extends MarkdownPage {
static const String routeName = '/privacy';
PrivacyPage(BuildContext context) PrivacyPage(BuildContext context, {super.key})
: super( : super(
title: S.of(context).privacyPolicy, title: S.of(context).privacyPolicy,
route: routeName, route: routeName,
@ -16,4 +15,5 @@ class PrivacyPage extends MarkdownPage {
fileName: 'privacy', fileName: 'privacy',
ext: 'md', ext: 'md',
lang: Localizations.localeOf(context).languageCode)); lang: Localizations.localeOf(context).languageCode));
static const String routeName = '/privacy';
} }

View file

@ -1,4 +1,4 @@
export 'appActions.dart'; export 'appActions.dart';
export 'yourLocationActions.dart';
export 'fireMapActions.dart'; export 'fireMapActions.dart';
export 'fireNotificationActions.dart'; export 'fireNotificationActions.dart';
export 'yourLocationActions.dart';

View file

@ -1,23 +1,25 @@
import 'package:fires_flutter/models/yourLocation.dart';
import 'package:fires_flutter/models/fireNotification.dart';
import 'dart:async'; import 'dart:async';
import 'package:flutter_map/flutter_map.dart';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter_map/flutter_map.dart';
import '../models/fireNotification.dart';
import '../models/yourLocation.dart';
abstract class AppActions {} abstract class AppActions {}
class FetchYourLocationsAction extends AppActions { class FetchYourLocationsAction extends AppActions {
Completer<Null>? refreshCallback;
FetchYourLocationsAction([this.refreshCallback]); FetchYourLocationsAction([this.refreshCallback]);
Completer<void>? refreshCallback;
} }
class PersistAppStateAction extends AppActions {} class PersistAppStateAction extends AppActions {}
class FetchYourLocationsSucceededAction extends AppActions { class FetchYourLocationsSucceededAction extends AppActions {
final List<YourLocation> fetchedYourLocations;
FetchYourLocationsSucceededAction(this.fetchedYourLocations); FetchYourLocationsSucceededAction(this.fetchedYourLocations);
final List<YourLocation> fetchedYourLocations;
} }
class FetchFireNotificationsAction extends AppActions {} class FetchFireNotificationsAction extends AppActions {}
@ -25,45 +27,45 @@ class FetchFireNotificationsAction extends AppActions {}
class FetchMonitoredAreasAction extends AppActions {} class FetchMonitoredAreasAction extends AppActions {}
class FetchYourLocationsFailedAction extends AppActions { class FetchYourLocationsFailedAction extends AppActions {
final Exception error;
FetchYourLocationsFailedAction(this.error); FetchYourLocationsFailedAction(this.error);
final Exception error;
} }
class OnUserTokenAction extends AppActions { class OnUserTokenAction extends AppActions {
final String token;
OnUserTokenAction(this.token); OnUserTokenAction(this.token);
final String token;
} }
class OnUserCreatedAction extends AppActions { class OnUserCreatedAction extends AppActions {
final String userId;
OnUserCreatedAction(this.userId); OnUserCreatedAction(this.userId);
final String userId;
} }
class OnUserLangAction extends AppActions { class OnUserLangAction extends AppActions {
final String lang;
OnUserLangAction(this.lang); OnUserLangAction(this.lang);
final String lang;
} }
class FetchFireNotificationsSucceededAction extends AppActions { class FetchFireNotificationsSucceededAction extends AppActions {
final List<FireNotification> fetchedFireNotifications;
final int unreadCount;
FetchFireNotificationsSucceededAction( FetchFireNotificationsSucceededAction(
this.fetchedFireNotifications, this.unreadCount); this.fetchedFireNotifications, this.unreadCount);
final List<FireNotification> fetchedFireNotifications;
final int unreadCount;
} }
class FetchMonitoredAreasSucceededAction extends AppActions { class FetchMonitoredAreasSucceededAction extends AppActions {
final List<Polyline> monitoredAreas;
FetchMonitoredAreasSucceededAction(this.monitoredAreas); FetchMonitoredAreasSucceededAction(this.monitoredAreas);
final List<Polyline> monitoredAreas;
} }
class OnConnectivityChanged extends AppActions { class OnConnectivityChanged extends AppActions {
final List<ConnectivityResult> connectionStatus;
OnConnectivityChanged(this.connectionStatus); OnConnectivityChanged(this.connectionStatus);
final List<ConnectivityResult> connectionStatus;
} }

View file

@ -1,5 +1,5 @@
import 'actions.dart';
import '../models/appState.dart'; import '../models/appState.dart';
import 'actions.dart';
AppState appReducer(AppState state, action) { AppState appReducer(AppState state, action) {
if (action is FetchYourLocationsSucceededAction) { if (action is FetchYourLocationsSucceededAction) {

View file

@ -1,14 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'package:fires_flutter/models/fireNotification.dart'; import 'package:flutter_map/src/layer/polyline_layer.dart';
import 'package:fires_flutter/models/yourLocation.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:objectid/objectid.dart'; import 'package:objectid/objectid.dart';
import 'package:redux/redux.dart'; import 'package:redux/redux.dart';
import '../models/appState.dart'; import '../models/appState.dart';
import '../models/fireNotification.dart';
import '../models/fireNotificationsPersist.dart'; import '../models/fireNotificationsPersist.dart';
import '../models/firesApi.dart'; import '../models/firesApi.dart';
import '../models/yourLocation.dart';
import '../models/yourLocationPersist.dart'; import '../models/yourLocationPersist.dart';
import '../objectIdUtils.dart'; import '../objectIdUtils.dart';
import 'actions.dart'; import 'actions.dart';
@ -55,20 +56,20 @@ void fetchDataMiddleware(Store<AppState> store, action, NextDispatcher next) {
if (action is AddYourLocationAction) { if (action is AddYourLocationAction) {
if (action.loc.subscribed) { if (action.loc.subscribed) {
subscribeViaApi(store, action.loc, (sub) { subscribeViaApi(store, action.loc, (YourLocation sub) {
store.dispatch(new AddedYourLocationAction(sub)); store.dispatch(AddedYourLocationAction(sub));
persistYourLocations(store.state.yourLocations); persistYourLocations(store.state.yourLocations);
}); });
} else { } else {
// No subscribed (only local) // No subscribed (only local)
store.dispatch(new AddedYourLocationAction(action.loc)); store.dispatch(AddedYourLocationAction(action.loc));
persistYourLocations(store.state.yourLocations); persistYourLocations(store.state.yourLocations);
} }
getFiresStatsInLocation(store, action.loc); getFiresStatsInLocation(store, action.loc);
} }
if (action is DeleteYourLocationAction) { if (action is DeleteYourLocationAction) {
store.dispatch(new DeletedYourLocationAction(action.loc.id)); store.dispatch(DeletedYourLocationAction(action.loc.id));
if (action.loc.subscribed) { if (action.loc.subscribed) {
unsubsViaApi(store, action.loc.id, () { unsubsViaApi(store, action.loc.id, () {
persistYourLocations(store.state.yourLocations); persistYourLocations(store.state.yourLocations);
@ -79,17 +80,17 @@ void fetchDataMiddleware(Store<AppState> store, action, NextDispatcher next) {
} }
if (action is DeleteFireNotificationAction) { if (action is DeleteFireNotificationAction) {
store.dispatch(new DeletedFireNotificationAction(action.notif)); store.dispatch(DeletedFireNotificationAction(action.notif));
persistFireNotifications(store.state.fireNotifications); persistFireNotifications(store.state.fireNotifications);
} }
if (action is DeleteAllFireNotificationAction) { if (action is DeleteAllFireNotificationAction) {
store.dispatch(new DeletedAllFireNotificationAction()); store.dispatch(DeletedAllFireNotificationAction());
persistFireNotifications(store.state.fireNotifications); persistFireNotifications(store.state.fireNotifications);
} }
if (action is AddFireNotificationAction) { if (action is AddFireNotificationAction) {
store.dispatch(new AddedFireNotificationAction(action.notif)); store.dispatch(AddedFireNotificationAction(action.notif));
persistFireNotifications(store.state.fireNotifications); persistFireNotifications(store.state.fireNotifications);
} }
@ -104,28 +105,28 @@ void fetchDataMiddleware(Store<AppState> store, action, NextDispatcher next) {
if (action is UpdateYourLocationAction) { if (action is UpdateYourLocationAction) {
if (action.loc.subscribed) if (action.loc.subscribed)
debounceLocationUpdate( debounceLocationUpdate(
Duration(seconds: 2), const Duration(seconds: 2),
() => api () => api
.getFiresInLocation( .getFiresInLocation(
state: store.state, state: store.state,
lat: action.loc.lat, lat: action.loc.lat,
lon: action.loc.lon, lon: action.loc.lon,
distance: action.loc.distance) distance: action.loc.distance)
.then((result) => store.dispatch(result))); .then((UpdateFireMapStatsAction result) => store.dispatch(result)));
store.dispatch(new UpdatedYourLocationAction(action.loc)); store.dispatch(UpdatedYourLocationAction(action.loc));
persistYourLocations(store.state.yourLocations); persistYourLocations(store.state.yourLocations);
} }
if (action is SubscribeConfirmAction) { if (action is SubscribeConfirmAction) {
subscribeViaApi(store, action.loc, (sub) { subscribeViaApi(store, action.loc, (YourLocation sub) {
store.dispatch(new UpdateYourLocationAction(action.loc)); store.dispatch(UpdateYourLocationAction(action.loc));
persistYourLocations(store.state.yourLocations); persistYourLocations(store.state.yourLocations);
}); });
} }
if (action is UnSubscribeAction) { if (action is UnSubscribeAction) {
unsubsViaApi(store, action.loc.id, () { unsubsViaApi(store, action.loc.id, () {
store.dispatch(new UpdateYourLocationAction(action.loc)); store.dispatch(UpdateYourLocationAction(action.loc));
persistYourLocations(store.state.yourLocations); persistYourLocations(store.state.yourLocations);
}); });
} }
@ -133,21 +134,21 @@ void fetchDataMiddleware(Store<AppState> store, action, NextDispatcher next) {
if (action is ToggleSubscriptionAction) { if (action is ToggleSubscriptionAction) {
if (action.loc.subscribed) { if (action.loc.subscribed) {
subscribeViaApi(store, action.loc, subscribeViaApi(store, action.loc,
(sub) => store.dispatch(new ToggledSubscriptionAction(sub))); (YourLocation sub) => store.dispatch(ToggledSubscriptionAction(sub)));
} else { } else {
unsubsViaApi(store, action.loc.id, unsubsViaApi(store, action.loc.id,
() => store.dispatch(new ToggledSubscriptionAction(action.loc))); () => store.dispatch(ToggledSubscriptionAction(action.loc)));
} }
} }
if (action is ReadFireNotificationAction) { if (action is ReadFireNotificationAction) {
store.dispatch(new ReadedFireNotificationAction(action.notif)); store.dispatch(ReadedFireNotificationAction(action.notif));
persistFireNotifications(store.state.fireNotifications); persistFireNotifications(store.state.fireNotifications);
} }
if (action is FetchYourLocationsAction) { if (action is FetchYourLocationsAction) {
// Use the api to fetch the YourLocations // Use the api to fetch the YourLocations
loadYourLocations().then((localLocations) { loadYourLocations().then((List<YourLocation> localLocations) {
api api
.fetchYourLocations(store.state) .fetchYourLocations(store.state)
.then((List<YourLocation> subscribedLocations) { .then((List<YourLocation> subscribedLocations) {
@ -155,61 +156,63 @@ void fetchDataMiddleware(Store<AppState> store, action, NextDispatcher next) {
// Our reducer will then update the State using these YourLocations. // Our reducer will then update the State using these YourLocations.
print('Subscribed to: ${subscribedLocations.length}'); print('Subscribed to: ${subscribedLocations.length}');
// unsubscribe all locally to sync the subs state // unsubscribe all locally to sync the subs state
localLocations.forEach((location) => location.subscribed = false); for (final YourLocation location in localLocations) {
location.subscribed = false;
}
print('Local persisted: ${localLocations.length}'); print('Local persisted: ${localLocations.length}');
subscribedLocations.forEach((subsLoc) { for (final YourLocation subsLoc in subscribedLocations) {
var locSubs = localLocations.firstWhere( final YourLocation locSubs = localLocations.firstWhere(
(localLocation) => localLocation.id == subsLoc.id, orElse: () { (YourLocation localLocation) => localLocation.id == subsLoc.id, orElse: () {
localLocations.add(subsLoc); localLocations.add(subsLoc);
return subsLoc; return subsLoc;
}); });
locSubs.subscribed = true; locSubs.subscribed = true;
}); }
store.dispatch(new FetchYourLocationsSucceededAction(localLocations)); store.dispatch(FetchYourLocationsSucceededAction(localLocations));
persistYourLocations(localLocations); persistYourLocations(localLocations);
localLocations.forEach((yl) { for (final YourLocation yl in localLocations) {
api api
.getFiresInLocation( .getFiresInLocation(
state: store.state, state: store.state,
lat: yl.lat, lat: yl.lat,
lon: yl.lon, lon: yl.lon,
distance: yl.distance) distance: yl.distance)
.then((value) { .then((UpdateFireMapStatsAction value) {
yl.currentNumFires = value.numFires; yl.currentNumFires = value.numFires;
store.dispatch(new UpdateYourLocationAction(yl)); store.dispatch(UpdateYourLocationAction(yl));
}); });
}); }
final Completer<Null>? completer = action.refreshCallback; final Completer<void>? completer = action.refreshCallback;
completer?.complete(null); completer?.complete(null);
}); });
}).catchError((Exception onError) { }).catchError((Exception onError) {
// If it fails, dispatch a failure action. The reducer will // If it fails, dispatch a failure action. The reducer will
// update the state with the error. // update the state with the error.
store.dispatch(new FetchYourLocationsFailedAction(onError)); store.dispatch(FetchYourLocationsFailedAction(onError));
}); });
} }
if (action is FetchFireNotificationsAction) { if (action is FetchFireNotificationsAction) {
loadFireNotifications().then((fireNotifications) { loadFireNotifications().then((List<FireNotification> fireNotifications) {
int unread = 0; int unread = 0;
for (FireNotification notif in fireNotifications) { for (final FireNotification notif in fireNotifications) {
if (!notif.read) { if (!notif.read) {
unread++; unread++;
} }
} }
store.dispatch( store.dispatch(
new FetchFireNotificationsSucceededAction(fireNotifications, unread)); FetchFireNotificationsSucceededAction(fireNotifications, unread));
persistFireNotifications(fireNotifications); persistFireNotifications(fireNotifications);
}); });
} }
if (action is FetchMonitoredAreasAction) { if (action is FetchMonitoredAreasAction) {
api.getMonitoredAreas(state: store.state).then((result) { api.getMonitoredAreas(state: store.state).then((List<Polyline> result) {
// store.dispatch() // store.dispatch()
store.dispatch(new FetchMonitoredAreasSucceededAction(result)); store.dispatch(FetchMonitoredAreasSucceededAction(result));
}); });
} }
@ -217,11 +220,11 @@ void fetchDataMiddleware(Store<AppState> store, action, NextDispatcher next) {
api api
.markFalsePositive(store.state, store.state.user.token, .markFalsePositive(store.state, store.state.user.token,
action.notif.sealed, action.type) action.notif.sealed, action.type)
.then((result) { .then((bool result) {
if (result) { if (result) {
// Not necessary // Not necessary
getFiresStatsInFire(store, action.notif); getFiresStatsInFire(store, action.notif);
store.dispatch(new UpdatedFireNotificationAction(action.notif)); store.dispatch(UpdatedFireNotificationAction(action.notif));
} }
}); });
} }
@ -237,10 +240,10 @@ void getFiresStatsInLocation(Store<AppState> store, YourLocation loc) {
lat: loc.lat, lat: loc.lat,
lon: loc.lon, lon: loc.lon,
distance: loc.distance) distance: loc.distance)
.then((result) { .then((UpdateFireMapStatsAction result) {
store.dispatch(result); store.dispatch(result);
loc.currentNumFires = result.numFires; loc.currentNumFires = result.numFires;
store.dispatch(new UpdateYourLocationAction(loc)); store.dispatch(UpdateYourLocationAction(loc));
}); });
} }
@ -251,11 +254,11 @@ void getFiresStatsInFire(Store<AppState> store, FireNotification notif) {
lat: notif.lat, lat: notif.lat,
lon: notif.lon, lon: notif.lon,
distance: 1) // FalsePositive/server/publications.js distance: 1) // FalsePositive/server/publications.js
.then((result) => store.dispatch(result)); .then((UpdateFireMapStatsAction result) => store.dispatch(result));
} }
void unsubsViaApi(Store<AppState> store, ObjectId id, onUnsubs) { void unsubsViaApi(Store<AppState> store, ObjectId id, onUnsubs) {
api.unsubscribe(store.state, id.hexString).then((res) { api.unsubscribe(store.state, id.hexString).then((bool res) {
onUnsubs(); onUnsubs();
persistYourLocations(store.state.yourLocations); persistYourLocations(store.state.yourLocations);
}); });
@ -263,8 +266,8 @@ void unsubsViaApi(Store<AppState> store, ObjectId id, onUnsubs) {
void subscribeViaApi( void subscribeViaApi(
Store<AppState> store, YourLocation loc, Function(YourLocation) onSubs) { Store<AppState> store, YourLocation loc, Function(YourLocation) onSubs) {
api.subscribe(store.state, loc).then((subsId) { api.subscribe(store.state, loc).then((String subsId) {
YourLocation sub = loc; final YourLocation sub = loc;
// if (loc.id != subsId) { // if (loc.id != subsId) {
sub.id = objectIdFromJson(subsId); sub.id = objectIdFromJson(subsId);
// } // }
@ -274,12 +277,12 @@ void subscribeViaApi(
} }
void createUser(Store<AppState> store, String lang, String token) { void createUser(Store<AppState> store, String lang, String token) {
assert(token != null, "User lang is null"); assert(token != null, 'User lang is null');
assert(token != null, "User mobile token is null"); assert(token != null, 'User mobile token is null');
api.createUser(store.state, token, lang).then((userId) { api.createUser(store.state, token, lang).then((String userId) {
store.dispatch(new OnUserCreatedAction(userId)); store.dispatch(OnUserCreatedAction(userId));
store.dispatch(new FetchYourLocationsAction()); store.dispatch(FetchYourLocationsAction());
store.dispatch(new FetchFireNotificationsAction()); store.dispatch(FetchFireNotificationsAction());
store.dispatch(new FetchMonitoredAreasAction()); store.dispatch(FetchMonitoredAreasAction());
}); });
} }

View file

@ -1,61 +1,61 @@
import 'package:fires_flutter/models/yourLocation.dart'; import '../models/fireMapState.dart';
import 'package:fires_flutter/models/fireNotification.dart'; import '../models/fireNotification.dart';
import 'package:fires_flutter/models/fireMapState.dart'; import '../models/yourLocation.dart';
abstract class FiresMapActions {} abstract class FiresMapActions {}
class UpdateYourLocationMapAction extends FiresMapActions { class UpdateYourLocationMapAction extends FiresMapActions {
final YourLocation loc;
UpdateYourLocationMapAction( UpdateYourLocationMapAction(
this.loc, this.loc,
); );
final YourLocation loc;
} }
class UpdateFireMapStatsAction extends FiresMapActions { class UpdateFireMapStatsAction extends FiresMapActions {
int numFires;
List<dynamic> fires = [];
List<dynamic> falsePos = [];
List<dynamic> industries = [];
UpdateFireMapStatsAction( UpdateFireMapStatsAction(
{required this.numFires, {required this.numFires,
required this.fires, required this.fires,
required this.falsePos, required this.falsePos,
required this.industries}); required this.industries});
int numFires;
List<dynamic> fires = <dynamic>[];
List<dynamic> falsePos = <dynamic>[];
List<dynamic> industries = <dynamic>[];
} }
class ShowYourLocationMapAction extends FiresMapActions { class ShowYourLocationMapAction extends FiresMapActions {
YourLocation loc;
ShowYourLocationMapAction(this.loc); ShowYourLocationMapAction(this.loc);
YourLocation loc;
} }
class ShowFireNotificationMapAction extends FiresMapActions { class ShowFireNotificationMapAction extends FiresMapActions {
FireNotification notif;
ShowFireNotificationMapAction(this.notif); ShowFireNotificationMapAction(this.notif);
FireNotification notif;
} }
class EditYourLocationAction extends FiresMapActions { class EditYourLocationAction extends FiresMapActions {
YourLocation loc;
EditYourLocationAction(this.loc); EditYourLocationAction(this.loc);
YourLocation loc;
} }
class EditConfirmYourLocationAction extends FiresMapActions { class EditConfirmYourLocationAction extends FiresMapActions {
YourLocation loc;
EditConfirmYourLocationAction(this.loc); EditConfirmYourLocationAction(this.loc);
YourLocation loc;
} }
class EditCancelYourLocationAction extends FiresMapActions { class EditCancelYourLocationAction extends FiresMapActions {
YourLocation loc;
EditCancelYourLocationAction(this.loc); EditCancelYourLocationAction(this.loc);
YourLocation loc;
} }
class SelectMapLayerAction extends FiresMapActions { class SelectMapLayerAction extends FiresMapActions {
final FireMapLayer layer;
SelectMapLayerAction(this.layer); SelectMapLayerAction(this.layer);
final FireMapLayer layer;
} }

View file

@ -1,28 +1,29 @@
import 'package:redux/redux.dart';
import 'package:fires_flutter/models/yourLocation.dart';
import 'package:objectid/objectid.dart'; import 'package:objectid/objectid.dart';
import 'package:redux/redux.dart';
import '../models/fireMapState.dart'; import '../models/fireMapState.dart';
import '../models/yourLocation.dart';
import 'actions.dart'; import 'actions.dart';
final fireMapReducer = combineReducers<FireMapState>([ final Reducer<FireMapState> fireMapReducer = combineReducers<FireMapState>(<Reducer<FireMapState>>[
new TypedReducer<FireMapState, ShowYourLocationMapAction>( TypedReducer<FireMapState, ShowYourLocationMapAction>(
_showYourLocationMap), _showYourLocationMap),
new TypedReducer<FireMapState, ShowFireNotificationMapAction>( TypedReducer<FireMapState, ShowFireNotificationMapAction>(
_showFireNotificationMap), _showFireNotificationMap),
new TypedReducer<FireMapState, UpdateFireMapStatsAction>( TypedReducer<FireMapState, UpdateFireMapStatsAction>(
_updateYourLocationMapStats), _updateYourLocationMapStats),
new TypedReducer<FireMapState, SubscribeAction>(_subscribeYourLocationMap), TypedReducer<FireMapState, SubscribeAction>(_subscribeYourLocationMap),
new TypedReducer<FireMapState, SubscribeConfirmAction>( TypedReducer<FireMapState, SubscribeConfirmAction>(
_subscribeConfirmYourLocationMap), _subscribeConfirmYourLocationMap),
new TypedReducer<FireMapState, UnSubscribeAction>( TypedReducer<FireMapState, UnSubscribeAction>(
_unsubscribeYourLocationMap), _unsubscribeYourLocationMap),
new TypedReducer<FireMapState, EditYourLocationAction>(_editYourLocationMap), TypedReducer<FireMapState, EditYourLocationAction>(_editYourLocationMap),
new TypedReducer<FireMapState, EditConfirmYourLocationAction>( TypedReducer<FireMapState, EditConfirmYourLocationAction>(
_editConfirmYourLocationMap), _editConfirmYourLocationMap),
new TypedReducer<FireMapState, EditCancelYourLocationAction>( TypedReducer<FireMapState, EditCancelYourLocationAction>(
_editCancelYourLocationMap), _editCancelYourLocationMap),
new TypedReducer<FireMapState, SelectMapLayerAction>(_selectMapLayer), TypedReducer<FireMapState, SelectMapLayerAction>(_selectMapLayer),
new TypedReducer<FireMapState, UpdateYourLocationMapAction>( TypedReducer<FireMapState, UpdateYourLocationMapAction>(
_updateYourLocationMap) _updateYourLocationMap)
]); ]);
@ -46,14 +47,13 @@ FireMapState _showYourLocationMap(
status: action.loc.subscribed status: action.loc.subscribed
? FireMapStatus.unsubscribe ? FireMapStatus.unsubscribe
: FireMapStatus.view, : FireMapStatus.view,
yourLocation: action.loc, yourLocation: action.loc);
fireNotication: null);
} }
FireMapState _showFireNotificationMap( FireMapState _showFireNotificationMap(
FireMapState state, ShowFireNotificationMapAction action) { FireMapState state, ShowFireNotificationMapAction action) {
// TODO: use here you real location? // TODO: use here you real location?
YourLocation pseudoLoc = new YourLocation( final YourLocation pseudoLoc = YourLocation(
id: ObjectId(), id: ObjectId(),
lat: action.notif.lat, lat: action.notif.lat,
lon: action.notif.lon, lon: action.notif.lon,

View file

@ -1,12 +1,12 @@
import 'package:fires_flutter/models/fireNotification.dart';
import '../models/falsePositiveTypes.dart'; import '../models/falsePositiveTypes.dart';
import '../models/fireNotification.dart';
abstract class FireNotificationActions {} abstract class FireNotificationActions {}
class DeleteFireNotificationAction extends FireNotificationActions { class DeleteFireNotificationAction extends FireNotificationActions {
final FireNotification notif;
DeleteFireNotificationAction(this.notif); DeleteFireNotificationAction(this.notif);
final FireNotification notif;
} }
class DeleteAllFireNotificationAction extends FireNotificationActions { class DeleteAllFireNotificationAction extends FireNotificationActions {
@ -14,15 +14,15 @@ class DeleteAllFireNotificationAction extends FireNotificationActions {
} }
class AddFireNotificationAction extends FireNotificationActions { class AddFireNotificationAction extends FireNotificationActions {
final FireNotification notif;
AddFireNotificationAction(this.notif); AddFireNotificationAction(this.notif);
final FireNotification notif;
} }
class DeletedFireNotificationAction extends FireNotificationActions { class DeletedFireNotificationAction extends FireNotificationActions {
final FireNotification notif;
DeletedFireNotificationAction(this.notif); DeletedFireNotificationAction(this.notif);
final FireNotification notif;
} }
class DeletedAllFireNotificationAction extends FireNotificationActions { class DeletedAllFireNotificationAction extends FireNotificationActions {
@ -30,32 +30,32 @@ class DeletedAllFireNotificationAction extends FireNotificationActions {
} }
class AddedFireNotificationAction extends FireNotificationActions { class AddedFireNotificationAction extends FireNotificationActions {
final FireNotification notif;
AddedFireNotificationAction(this.notif); AddedFireNotificationAction(this.notif);
final FireNotification notif;
} }
class ReadFireNotificationAction extends FireNotificationActions { class ReadFireNotificationAction extends FireNotificationActions {
final FireNotification notif;
ReadFireNotificationAction(this.notif); ReadFireNotificationAction(this.notif);
final FireNotification notif;
} }
class ReadedFireNotificationAction extends FireNotificationActions { class ReadedFireNotificationAction extends FireNotificationActions {
final FireNotification notif;
ReadedFireNotificationAction(this.notif); ReadedFireNotificationAction(this.notif);
final FireNotification notif;
} }
class MarkFireAsFalsePositiveAction extends FireNotificationActions { class MarkFireAsFalsePositiveAction extends FireNotificationActions {
final FireNotification notif;
final FalsePositiveType type;
MarkFireAsFalsePositiveAction(this.notif, this.type); MarkFireAsFalsePositiveAction(this.notif, this.type);
final FireNotification notif;
final FalsePositiveType type;
} }
class UpdatedFireNotificationAction extends FireNotificationActions { class UpdatedFireNotificationAction extends FireNotificationActions {
final FireNotification notif;
UpdatedFireNotificationAction(this.notif); UpdatedFireNotificationAction(this.notif);
final FireNotification notif;
} }

View file

@ -1,44 +1,44 @@
import 'package:fires_flutter/models/fireNotification.dart';
import 'package:redux/redux.dart'; import 'package:redux/redux.dart';
import '../models/fireNotification.dart';
import 'actions.dart'; import 'actions.dart';
final fireNotificationReducer = combineReducers<List<FireNotification>>([ final Reducer<List<FireNotification>> fireNotificationReducer = combineReducers<List<FireNotification>>(<Reducer<List<FireNotification>>>[
new TypedReducer<List<FireNotification>, AddedFireNotificationAction>( TypedReducer<List<FireNotification>, AddedFireNotificationAction>(
_addedFireNotification), _addedFireNotification),
new TypedReducer<List<FireNotification>, DeletedFireNotificationAction>( TypedReducer<List<FireNotification>, DeletedFireNotificationAction>(
_deletedFireNotification), _deletedFireNotification),
new TypedReducer<List<FireNotification>, UpdatedFireNotificationAction>( TypedReducer<List<FireNotification>, UpdatedFireNotificationAction>(
_updatedFireNotification), _updatedFireNotification),
new TypedReducer<List<FireNotification>, DeletedAllFireNotificationAction>( TypedReducer<List<FireNotification>, DeletedAllFireNotificationAction>(
_deletedAllFireNotifications), _deletedAllFireNotifications),
new TypedReducer<List<FireNotification>, ReadedFireNotificationAction>( TypedReducer<List<FireNotification>, ReadedFireNotificationAction>(
_readedFireNotification) _readedFireNotification)
]); ]);
List<FireNotification> _addedFireNotification( List<FireNotification> _addedFireNotification(
List<FireNotification> notifications, AddedFireNotificationAction action) { List<FireNotification> notifications, AddedFireNotificationAction action) {
return new List.from(notifications)..insert(0, action.notif); return List.from(notifications)..insert(0, action.notif);
} }
List<FireNotification> _deletedFireNotification( List<FireNotification> _deletedFireNotification(
List<FireNotification> notifications, List<FireNotification> notifications,
DeletedFireNotificationAction action) { DeletedFireNotificationAction action) {
return new List.from(notifications)..remove(action.notif); return List.from(notifications)..remove(action.notif);
} }
List<FireNotification> _updatedFireNotification( List<FireNotification> _updatedFireNotification(
List<FireNotification> notifications, List<FireNotification> notifications,
UpdatedFireNotificationAction action) { UpdatedFireNotificationAction action) {
return notifications return notifications
.map((notif) => notif.id == action.notif.id ? action.notif : notif) .map((FireNotification notif) => notif.id == action.notif.id ? action.notif : notif)
.toList(); .toList();
} }
List<FireNotification> _readedFireNotification( List<FireNotification> _readedFireNotification(
List<FireNotification> notifications, ReadedFireNotificationAction action) { List<FireNotification> notifications, ReadedFireNotificationAction action) {
return notifications return notifications
.map((yourLocation) => .map((FireNotification yourLocation) =>
yourLocation.id == action.notif.id ? action.notif : yourLocation) yourLocation.id == action.notif.id ? action.notif : yourLocation)
.toList(); .toList();
} }
@ -46,5 +46,5 @@ List<FireNotification> _readedFireNotification(
List<FireNotification> _deletedAllFireNotifications( List<FireNotification> _deletedAllFireNotifications(
List<FireNotification> notifications, List<FireNotification> notifications,
DeletedAllFireNotificationAction action) { DeletedAllFireNotificationAction action) {
return []; return <FireNotification>[];
} }

View file

@ -10,7 +10,7 @@ import 'yourLocationsReducer.dart';
// We create the State reducer by combining many smaller reducers into one! // We create the State reducer by combining many smaller reducers into one!
AppState appStateReducer(AppState prevState, action) { AppState appStateReducer(AppState prevState, action) {
var state = appReducer(prevState, action); final AppState state = appReducer(prevState, action);
return AppState( return AppState(
yourLocations: yourLocationsReducer(state.yourLocations, action), yourLocations: yourLocationsReducer(state.yourLocations, action),
fireNotifications: fireNotifications:

View file

@ -1,5 +1,5 @@
import 'actions.dart';
import '../models/user.dart'; import '../models/user.dart';
import 'actions.dart';
User userReducer(User user, action) { User userReducer(User user, action) {
if (action is OnUserCreatedAction) return user.copyWith(userId: action.userId); if (action is OnUserCreatedAction) return user.copyWith(userId: action.userId);

View file

@ -1,66 +1,67 @@
import 'package:fires_flutter/models/yourLocation.dart';
import 'package:objectid/objectid.dart'; import 'package:objectid/objectid.dart';
import '../models/yourLocation.dart';
abstract class YourLocationActions {} abstract class YourLocationActions {}
class AddYourLocationAction extends YourLocationActions { class AddYourLocationAction extends YourLocationActions {
YourLocation loc;
AddYourLocationAction(this.loc); AddYourLocationAction(this.loc);
YourLocation loc;
} }
class AddedYourLocationAction extends YourLocationActions { class AddedYourLocationAction extends YourLocationActions {
YourLocation loc;
AddedYourLocationAction(this.loc); AddedYourLocationAction(this.loc);
YourLocation loc;
} }
class DeleteYourLocationAction extends YourLocationActions { class DeleteYourLocationAction extends YourLocationActions {
YourLocation loc;
DeleteYourLocationAction(this.loc); DeleteYourLocationAction(this.loc);
YourLocation loc;
} }
class DeletedYourLocationAction extends YourLocationActions { class DeletedYourLocationAction extends YourLocationActions {
ObjectId id;
DeletedYourLocationAction(this.id); DeletedYourLocationAction(this.id);
ObjectId id;
} }
class UpdateYourLocationAction extends YourLocationActions { class UpdateYourLocationAction extends YourLocationActions {
YourLocation loc;
UpdateYourLocationAction(this.loc); UpdateYourLocationAction(this.loc);
YourLocation loc;
} }
class UpdatedYourLocationAction extends YourLocationActions { class UpdatedYourLocationAction extends YourLocationActions {
YourLocation loc;
UpdatedYourLocationAction(this.loc); UpdatedYourLocationAction(this.loc);
YourLocation loc;
} }
class ToggleSubscriptionAction extends YourLocationActions { class ToggleSubscriptionAction extends YourLocationActions {
YourLocation loc;
ToggleSubscriptionAction(this.loc); ToggleSubscriptionAction(this.loc);
YourLocation loc;
} }
class ToggledSubscriptionAction extends YourLocationActions { class ToggledSubscriptionAction extends YourLocationActions {
YourLocation loc;
ToggledSubscriptionAction(this.loc); ToggledSubscriptionAction(this.loc);
YourLocation loc;
} }
class SubscribeAction extends YourLocationActions {} class SubscribeAction extends YourLocationActions {}
class SubscribeConfirmAction extends YourLocationActions { class SubscribeConfirmAction extends YourLocationActions {
YourLocation loc;
SubscribeConfirmAction(this.loc); SubscribeConfirmAction(this.loc);
YourLocation loc;
} }
class UnSubscribeAction extends YourLocationActions { class UnSubscribeAction extends YourLocationActions {
YourLocation loc;
UnSubscribeAction(this.loc); UnSubscribeAction(this.loc);
YourLocation loc;
} }

View file

@ -1,35 +1,35 @@
import 'package:fires_flutter/models/yourLocation.dart';
import 'package:redux/redux.dart'; import 'package:redux/redux.dart';
import '../models/yourLocation.dart';
import 'actions.dart'; import 'actions.dart';
final yourLocationsReducer = combineReducers<List<YourLocation>>([ final Reducer<List<YourLocation>> yourLocationsReducer = combineReducers<List<YourLocation>>(<Reducer<List<YourLocation>>>[
new TypedReducer<List<YourLocation>, AddedYourLocationAction>( TypedReducer<List<YourLocation>, AddedYourLocationAction>(
_addedYourLocation), _addedYourLocation),
new TypedReducer<List<YourLocation>, DeletedYourLocationAction>( TypedReducer<List<YourLocation>, DeletedYourLocationAction>(
_deletedYourLocation), _deletedYourLocation),
new TypedReducer<List<YourLocation>, UpdatedYourLocationAction>( TypedReducer<List<YourLocation>, UpdatedYourLocationAction>(
_updatedYourLocation), _updatedYourLocation),
new TypedReducer<List<YourLocation>, ToggledSubscriptionAction>( TypedReducer<List<YourLocation>, ToggledSubscriptionAction>(
_toggledSubscriptionAction) _toggledSubscriptionAction)
]); ]);
List<YourLocation> _addedYourLocation( List<YourLocation> _addedYourLocation(
List<YourLocation> yourLocations, AddedYourLocationAction action) { List<YourLocation> yourLocations, AddedYourLocationAction action) {
return new List.from(yourLocations)..add(action.loc); return List.from(yourLocations)..add(action.loc);
} }
List<YourLocation> _deletedYourLocation( List<YourLocation> _deletedYourLocation(
List<YourLocation> yourLocations, DeletedYourLocationAction action) { List<YourLocation> yourLocations, DeletedYourLocationAction action) {
return yourLocations return yourLocations
.where((yourLocation) => yourLocation.id != action.id) .where((YourLocation yourLocation) => yourLocation.id != action.id)
.toList(); .toList();
} }
List<YourLocation> _updatedYourLocation( List<YourLocation> _updatedYourLocation(
List<YourLocation> yourLocations, UpdatedYourLocationAction action) { List<YourLocation> yourLocations, UpdatedYourLocationAction action) {
return yourLocations return yourLocations
.map((yourLocation) => .map((YourLocation yourLocation) =>
yourLocation.id == action.loc.id ? action.loc : yourLocation) yourLocation.id == action.loc.id ? action.loc : yourLocation)
.toList(); .toList();
} }
@ -37,7 +37,7 @@ List<YourLocation> _updatedYourLocation(
List<YourLocation> _toggledSubscriptionAction( List<YourLocation> _toggledSubscriptionAction(
List<YourLocation> yourLocations, ToggledSubscriptionAction action) { List<YourLocation> yourLocations, ToggledSubscriptionAction action) {
return yourLocations return yourLocations
.map((yourLocation) => .map((YourLocation yourLocation) =>
yourLocation.id == action.loc.id ? action.loc : yourLocation) yourLocation.id == action.loc.id ? action.loc : yourLocation)
.toList(); .toList();
} }

View file

@ -7,31 +7,32 @@ new RandomColorBlock( child
*/ */
class Sandbox extends StatelessWidget { class Sandbox extends StatelessWidget {
const Sandbox({super.key});
static const String routeName = '/sandbox'; static const String routeName = '/sandbox';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
//showDialog(context: context, child: builder(context)); //showDialog(context: context, child: builder(context));
return Scaffold( return Scaffold(
appBar: new AppBar( appBar: AppBar(
title: new TextField( title: TextField(
autofocus: true, autofocus: true,
controller: new TextEditingController(text: "kk"), controller: TextEditingController(text: 'kk'),
decoration: new InputDecoration(), onSubmitted: (String todoText) {},
onSubmitted: (todoText) {},
)), )),
body: new ElevatedButton( body: ElevatedButton(
child: new Text('Press'), child: const Text('Press'),
onPressed: () { onPressed: () {
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (BuildContext context) {
return new SimpleDialog( return const SimpleDialog(
title: new Text('title'), title: Text('title'),
children: <Widget>[ children: <Widget>[
new Padding( Padding(
padding: const EdgeInsets.only(left: 16.0), padding: EdgeInsets.only(left: 16.0),
child: new Text('hhh'), child: Text('hhh'),
), ),
]); ]);
}); });

View file

@ -4,9 +4,9 @@ import 'package:sentry_flutter/sentry_flutter.dart';
import 'globals.dart' as globals; import 'globals.dart' as globals;
var useSentry = !globals.isDevelopment; bool useSentry = !globals.isDevelopment;
Future<Null> reportError(dynamic error, dynamic stackTrace) async { Future<void> reportError(dynamic error, dynamic stackTrace) async {
// Print the exception to the console // Print the exception to the console
print('Caught error: $error'); print('Caught error: $error');

View file

@ -4,61 +4,61 @@ import 'package:flutter/material.dart';
import 'colors.dart'; import 'colors.dart';
import 'generated/i18n.dart'; import 'generated/i18n.dart';
typedef void SlideCallback(int distance); typedef SlideCallback = void Function(int distance);
class FireDistanceSlider extends StatefulWidget { class FireDistanceSlider extends StatefulWidget {
const FireDistanceSlider({super.key, required this.initialValue, required this.onSlide});
final int initialValue; final int initialValue;
final SlideCallback onSlide; final SlideCallback onSlide;
FireDistanceSlider({required this.initialValue, required this.onSlide});
@override @override
_FireDistanceSliderState createState() => new _FireDistanceSliderState( _FireDistanceSliderState createState() => _FireDistanceSliderState(
initialValue: initialValue, onSlide: onSlide); initialValue: initialValue, onSlide: onSlide);
} }
class _FireDistanceSliderState extends State<FireDistanceSlider> { class _FireDistanceSliderState extends State<FireDistanceSlider> {
_FireDistanceSliderState({int initialValue = 10, required this.onSlide}) {
_sliderValue = initialValue;
}
late int _sliderValue; late int _sliderValue;
final SlideCallback onSlide; final SlideCallback onSlide;
_FireDistanceSliderState({int initialValue = 10, required this.onSlide}) { Widget sizeText(int sliderValue) => Text(
this._sliderValue = initialValue;
}
Widget sizeText(int sliderValue) => new Text(
S.of(context).subscribeToValueAroundThisArea(sliderValue.toString()), S.of(context).subscribeToValueAroundThisArea(sliderValue.toString()),
style: new TextStyle(color: Colors.black87)); style: const TextStyle(color: Colors.black87));
Widget warningText(int sliderValue) => _sliderValue >= 50 Widget warningText(int sliderValue) => _sliderValue >= 50
? new Text(S.of(context).warningThisIsAVeryLargeArea, ? Text(S.of(context).warningThisIsAVeryLargeArea,
style: new TextStyle(color: fires900)) style: const TextStyle(color: fires900))
: new Text(''); : const Text('');
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var slider = new Slider( final Slider slider = Slider(
value: _sliderValue + 0.0, value: _sliderValue + 0.0,
activeColor: fires900, activeColor: fires900,
inactiveColor: Colors.black45, inactiveColor: Colors.black45,
min: 1.0, min: 1.0,
max: 100.0, max: 100.0,
divisions: 99, divisions: 99,
label: '${_sliderValue.round()}', label: '$_sliderValue',
onChanged: (double value) { onChanged: (double value) {
_sliderValue = value.round(); _sliderValue = value.round();
onSlide(_sliderValue); onSlide(_sliderValue);
setState(() {}); setState(() {});
}, },
); );
return new Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: listWithoutNulls(<Widget>[ children: listWithoutNulls(<Widget>[
new SizedBox(height: 50.0), const SizedBox(height: 50.0),
new Row(mainAxisSize: MainAxisSize.max, children: <Widget>[slider]), Row(children: <Widget>[slider]),
// new SizedBox(height: 5.0), // new SizedBox(height: 5.0),
new Column(children: <Widget>[ Column(children: <Widget>[
sizeText(_sliderValue), sizeText(_sliderValue),
new SizedBox(height: 5.0), const SizedBox(height: 5.0),
warningText(_sliderValue) warningText(_sliderValue)
]) ])
]), ]),

View file

@ -1,13 +1,15 @@
import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:in_app_review/in_app_review.dart'; import 'package:in_app_review/in_app_review.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'generated/i18n.dart'; import 'generated/i18n.dart';
import 'mainDrawer.dart'; import 'mainDrawer.dart';
class SupportPage extends StatefulWidget { class SupportPage extends StatefulWidget {
const SupportPage({super.key});
static const String routeName = '/support'; static const String routeName = '/support';
@override @override
@ -15,27 +17,27 @@ class SupportPage extends StatefulWidget {
} }
class _SupportPageState extends State<SupportPage> { class _SupportPageState extends State<SupportPage> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
Widget buildSupportButton() { Widget buildSupportButton() {
return new Align( return Align(
alignment: const Alignment(0.0, -0.2), alignment: const Alignment(0.0, -0.2),
child: new OutlinedButton.icon( child: OutlinedButton.icon(
icon: const Icon(Icons.favorite_border), icon: const Icon(Icons.favorite_border),
label: new Text(S.of(context).comunesSupportBtn), label: Text(S.of(context).comunesSupportBtn),
onPressed: () { onPressed: () {
launch("https://comunes.org/"); launch('https://comunes.org/');
}, },
), ),
); );
} }
Widget buildShareButton() { Widget buildShareButton() {
return new Align( return Align(
alignment: const Alignment(0.0, -0.2), alignment: const Alignment(0.0, -0.2),
child: new OutlinedButton.icon( child: OutlinedButton.icon(
icon: const Icon(Icons.share), icon: const Icon(Icons.share),
label: new Text(S.of(context).shareAppBtn), label: Text(S.of(context).shareAppBtn),
onPressed: () { onPressed: () {
Share.shareWithResult( Share.shareWithResult(
'https://play.google.com/store/apps/details?id=org.comunes.fires'); 'https://play.google.com/store/apps/details?id=org.comunes.fires');
@ -45,11 +47,11 @@ class _SupportPageState extends State<SupportPage> {
} }
Widget buildStarButton() { Widget buildStarButton() {
return new Align( return Align(
alignment: const Alignment(0.0, -0.2), alignment: const Alignment(0.0, -0.2),
child: new OutlinedButton.icon( child: OutlinedButton.icon(
icon: const Icon(Icons.star_border), icon: const Icon(Icons.star_border),
label: new Text(S.of(context).starAppBtn), label: Text(S.of(context).starAppBtn),
onPressed: () { onPressed: () {
InAppReview.instance.requestReview(); InAppReview.instance.requestReview();
}, },
@ -58,14 +60,14 @@ class _SupportPageState extends State<SupportPage> {
} }
Widget buildTranslateButton() { Widget buildTranslateButton() {
return new Align( return Align(
alignment: const Alignment(0.0, -0.2), alignment: const Alignment(0.0, -0.2),
child: new OutlinedButton.icon( child: OutlinedButton.icon(
icon: const Icon(Icons.translate), icon: const Icon(Icons.translate),
label: new Text(S.of(context).translateBtn), label: Text(S.of(context).translateBtn),
onPressed: () { onPressed: () {
launch( launch(
"https://translate.comunes.org/projects/todos-contra-el-fuego/"); 'https://translate.comunes.org/projects/todos-contra-el-fuego/');
}, },
), ),
); );
@ -76,24 +78,24 @@ class _SupportPageState extends State<SupportPage> {
return Scaffold( return Scaffold(
key: _scaffoldKey, key: _scaffoldKey,
appBar: appBar:
new AppBar(title: new Text(S.of(context).supportThisInitiative)), AppBar(title: Text(S.of(context).supportThisInitiative)),
drawer: new MainDrawer(context, SupportPage.routeName), drawer: MainDrawer(context, SupportPage.routeName),
body: Padding( body: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: new CenteredColumn(children: <Widget>[ child: CenteredColumn(children: <Widget>[
new Flexible( Flexible(
child: new CenteredColumn(children: <Widget>[ child: CenteredColumn(children: <Widget>[
new Text( Text(
S.of(context).supportPageDescription, S.of(context).supportPageDescription,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
new SizedBox(height: 20.0), const SizedBox(height: 20.0),
buildSupportButton(), buildSupportButton(),
new SizedBox(height: 20.0), const SizedBox(height: 20.0),
buildTranslateButton(), buildTranslateButton(),
new SizedBox(height: 20.0), const SizedBox(height: 20.0),
buildStarButton(), buildStarButton(),
new SizedBox(height: 20.0), const SizedBox(height: 20.0),
buildShareButton() buildShareButton()
])) ]))
]), ]),

View file

@ -10,16 +10,14 @@ ThemeData _buildFiresTheme() {
primaryColor: fires600, primaryColor: fires600,
scaffoldBackgroundColor: firesSurfaceWhite, scaffoldBackgroundColor: firesSurfaceWhite,
cardColor: firesBackgroundWhite, cardColor: firesBackgroundWhite,
textSelectionTheme: TextSelectionThemeData(selectionColor: fires300), textSelectionTheme: const TextSelectionThemeData(selectionColor: fires300),
colorScheme: ColorScheme.light( colorScheme: const ColorScheme.light(
primary: fires600, primary: fires600,
secondary: fires900, secondary: fires900,
error: firesErrorRed, error: firesErrorRed,
surface: firesSurfaceWhite, surface: firesSurfaceWhite,
onSurface: fires900, onSurface: fires900,
onPrimary: Colors.white,
onSecondary: Colors.white, onSecondary: Colors.white,
onError: Colors.white,
), ),
//TODO: Add the text themes (103) //TODO: Add the text themes (103)
//TODO: Add the icon themes (103) //TODO: Add the icon themes (103)

View file

@ -10,16 +10,14 @@ ThemeData _buildFiresTheme() {
primaryColor: Colors.pink, primaryColor: Colors.pink,
scaffoldBackgroundColor: firesSurfaceWhite, scaffoldBackgroundColor: firesSurfaceWhite,
cardColor: firesBackgroundWhite, cardColor: firesBackgroundWhite,
textSelectionTheme: TextSelectionThemeData(selectionColor: fires300), textSelectionTheme: const TextSelectionThemeData(selectionColor: fires300),
colorScheme: ColorScheme.light( colorScheme: const ColorScheme.light(
primary: Colors.pink, primary: Colors.pink,
secondary: fires900, secondary: fires900,
error: firesErrorRed, error: firesErrorRed,
surface: firesSurfaceWhite, surface: firesSurfaceWhite,
onSurface: fires900, onSurface: fires900,
onPrimary: Colors.white,
onSecondary: Colors.white, onSecondary: Colors.white,
onError: Colors.white,
), ),
//TODO: Add the text themes (103) //TODO: Add the text themes (103)
//TODO: Add the icon themes (103) //TODO: Add the icon themes (103)

View file

@ -1,20 +1,23 @@
import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
/// Zoom control widget for flutter_map v6+ /// Zoom control widget for flutter_map v6+
class ZoomMapPluginWidget extends StatelessWidget { class ZoomMapPluginWidget extends StatelessWidget {
const ZoomMapPluginWidget({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) => builder: (BuildContext context, BoxConstraints constraints) =>
Stack(fit: StackFit.expand, children: <Widget>[ Stack(fit: StackFit.expand, children: <Widget>[
Positioned( Positioned(
top: constraints.maxHeight - 100, top: constraints.maxHeight - 100,
right: 10.0, right: 10.0,
child: new CenteredRow( child: CenteredRow(
children: <Widget>[ children: <Widget>[
new Column( Column(
children: <Widget>[ children: <Widget>[
_zoomButton(context, Icons.zoom_in, 1), _zoomButton(context, Icons.zoom_in, 1),
_zoomButton(context, Icons.zoom_out, -1) _zoomButton(context, Icons.zoom_out, -1)
@ -27,12 +30,12 @@ class ZoomMapPluginWidget extends StatelessWidget {
} }
IconButton _zoomButton(BuildContext context, IconData zoomIcon, int inc) { IconButton _zoomButton(BuildContext context, IconData zoomIcon, int inc) {
return new IconButton( return IconButton(
icon: Icon(zoomIcon), icon: Icon(zoomIcon),
onPressed: () { onPressed: () {
final controller = MapController.of(context); final MapController controller = MapController.of(context);
var currentZoom = controller.camera.zoom; final double currentZoom = controller.camera.zoom;
var currentCenter = controller.camera.center; final LatLng currentCenter = controller.camera.center;
controller.move(currentCenter, currentZoom + inc); controller.move(currentCenter, currentZoom + inc);
}); });
} }

View file

@ -1,29 +1,29 @@
import 'package:test/test.dart';
import 'package:fires_flutter/fileUtils.dart'; import 'package:fires_flutter/fileUtils.dart';
import 'package:test/test.dart';
void main() { void main() {
test('test es-ES fallback', () { test('test es-ES fallback', () {
String answer = getFallbackLang('es-ES'); final String answer = getFallbackLang('es-ES');
expect(answer, 'es'); expect(answer, 'es');
}); });
test('test en-GB fallback', () { test('test en-GB fallback', () {
String answer = getFallbackLang('en-GB'); final String answer = getFallbackLang('en-GB');
expect(answer, 'en'); expect(answer, 'en');
}); });
test('test gl fallback', () { test('test gl fallback', () {
String answer = getFallbackLang('gl'); final String answer = getFallbackLang('gl');
expect(answer, 'es'); expect(answer, 'es');
}); });
test('test fr fallback', () { test('test fr fallback', () {
String answer = getFallbackLang('fr'); final String answer = getFallbackLang('fr');
expect(answer, 'en'); expect(answer, 'en');
}); });
test('test privacy English md page', () async { test('test privacy English md page', () async {
String answer = await getFileNameOfLang(dir: 'assets/pages', fileName: 'privacy', ext: 'md', lang: 'en'); final String answer = await getFileNameOfLang(dir: 'assets/pages', fileName: 'privacy', ext: 'md', lang: 'en');
expect(answer, 'assets/pages/privacy-en.md'); expect(answer, 'assets/pages/privacy-en.md');
}); });