todos-contra-el-fuego-mobile/lib/active_fires.dart
vjrj 30a89fc5c0 Fix 13 lint issues: immutability, imports, async/await, and code style
High-priority fixes (10 issues):
- assignment_to_final errors (7): Use copyWith() to respect immutability in YourLocation, active_fires.dart, generic_map.dart, location_utils.dart, and fetch_data_middleware.dart instead of direct assignment
- Fixed import paths (3): Updated falsePositiveTypes.dart → false_positive_types.dart and firesApi.dart → fires_api.dart in reducers.dart, fire_notification_actions.dart, fires_api.dart

Medium/Low priority fixes (3 issues):
- unnecessary_non_null_assertion: Removed redundant '!' in app_intro_page.dart line 54
- use_super_parameters: Updated app_intro_page.dart to use modern super(key: key) syntax
- no_logic_in_create_state: Fixed material_app_with_intro.dart by moving parameters to State instead of createState
- noop_primitive_operations: Removed redundant .toString() in string interpolation
- avoid_void_async: Changed void _selectLocation to Future<void> in places_autocomplete_utils.dart
- avoid_dynamic_calls: Fixed dynamic map access in fires_api.dart by properly casting intermediate values

Result: Reduced lint issues from 56 to 20 (all remaining are by-design: library_private_types_in_public_api and implementation_imports from external packages)
APK builds successfully with no errors.
2026-03-12 09:16:20 +01:00

314 lines
11 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
import 'package:redux/redux.dart';
import 'colors.dart';
import 'fires_spinner.dart';
import 'generated/i18n.dart';
import 'generic_map.dart';
import 'global_fires_bottom_stats.dart';
import 'location_utils.dart';
import 'main_drawer.dart';
import 'models/app_state.dart';
import 'models/your_location.dart';
import 'places_autocomplete_utils.dart';
import 'redux/actions.dart';
import 'widgets/rounded_btn.dart';
@immutable
class _ViewModel {
const _ViewModel(
{required this.onAdd,
required this.onDelete,
required this.onToggleSubs,
required this.onTap,
required this.onRefresh,
required this.yourLocations,
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
bool operator ==(Object other) =>
identical(this, other) ||
other is _ViewModel &&
runtimeType == other.runtimeType &&
yourLocations == other.yourLocations &&
isLoading == other.isLoading;
@override
int get hashCode => yourLocations.hashCode ^ isLoading.hashCode;
}
class ActiveFiresPage extends StatefulWidget {
const ActiveFiresPage({super.key});
static const String routeName = '/fires';
@override
_ActiveFiresPageState createState() => _ActiveFiresPageState();
}
class _ActiveFiresPageState extends State<ActiveFiresPage> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
List<SpeedDialChild> _fabMiniMenuItemList(
BuildContext context, AddYourLocationFunction onAdd) {
return <SpeedDialChild>[
SpeedDialChild(
child: const Icon(Icons.location_searching),
backgroundColor: fires600,
label: S.of(context).addYourCurrentPosition,
labelWidget: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Container(
color: Colors.white,
child: Text(S.of(context).addYourCurrentPosition,
style: const TextStyle(
color: Colors.black87, fontWeight: FontWeight.w500)),
),
),
onTap: () {
onAddYourLocation(onAdd);
},
),
SpeedDialChild(
child: const Icon(Icons.edit_location),
backgroundColor: fires600,
label: S.of(context).addSomePlace,
labelWidget: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Container(
color: Colors.white,
child: Text(S.of(context).addSomePlace,
style: const TextStyle(
color: Colors.black87, fontWeight: FontWeight.w500)),
),
),
onTap: () {
onAddOtherLocation(onAdd);
},
)
];
}
Widget _buildRow(BuildContext context, YourLocation loc,
ToggleSubscriptionFunction onToggle, OnLocationTapFunction onTap) {
const TextStyle fireStatsStyle = TextStyle(color: fires600);
return ListTile(
dense: true,
isThreeLine: false,
// leading: const Icon(Icons.location_on),
trailing: IconButton(
icon: Icon(loc.subscribed
? Icons.notifications_active
: Icons.notifications_off),
color: loc.subscribed ? fires600 : null,
onPressed: () {
final YourLocation updatedLoc =
loc.copyWith(subscribed: !loc.subscribed);
onToggle(updatedLoc);
setState(() {});
showSnackMsg(updatedLoc.subscribed
? S.of(context).subscribedToFires
: S.of(context).unsubscribedToFires);
}),
title: Text(loc.description, style: const TextStyle(fontSize: 16.0)),
subtitle: loc.currentNumFires == YourLocation.withoutStats
? null
: loc.currentNumFires == 0
? Text(S.of(context).noFiresAround)
: loc.currentNumFires == 1
? Text(
S
.of(context)
.fireAroundThisArea(loc.distance.toString()),
style: fireStatsStyle)
: Text(
S.of(context).firesAroundThisArea(
loc.currentNumFires.toString(),
loc.distance.toString()),
style: fireStatsStyle),
onLongPress: () {
showSnackMsg(S.of(context).toDeleteThisPlace);
},
onTap: () {
onTap(loc);
});
}
void showSnackMsg(String msg) {
// _scaffoldKey.currentState.showSnackBar(new SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(msg),
));
}
Widget _buildSavedLocations(
BuildContext context,
List<YourLocation> yl,
DeleteYourLocationFunction onDeleted,
ToggleSubscriptionFunction onToggle,
OnLocationTapFunction onTap) {
return ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: yl.length,
itemBuilder: (BuildContext context, int i) {
final ThemeData theme = Theme.of(context);
return Dismissible(
key: ObjectKey(yl.elementAt(i)),
onDismissed: (DismissDirection direction) {
onDeleted(yl.elementAt(i));
},
background: Container(
color: theme.primaryColor,
child: const ListTile(
leading:
Icon(Icons.delete, color: Colors.white, size: 36.0))),
secondaryBackground: Container(
color: theme.primaryColor,
child: const ListTile(
trailing:
Icon(Icons.delete, color: Colors.white, size: 36.0))),
child: Container(
decoration: BoxDecoration(
color: theme.canvasColor,
border: Border(
bottom: BorderSide(color: theme.dividerColor))),
child: _buildRow(context, yl.elementAt(i), onToggle, onTap)));
});
}
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, _ViewModel>(
distinct: true,
converter: (Store<AppState> store) {
return _ViewModel(
onAdd: (YourLocation loc) {
if (store.state.yourLocations.any(
(YourLocation l) => loc.lat == l.lat && loc.lon == l.lon)) {
// Already added
showSnackMsg(S.of(context).addedThisLocation);
} else {
store.dispatch(AddYourLocationAction(loc));
Timer(const Duration(milliseconds: 1000), () {
gotoLocationMap(store, loc, context);
});
}
},
onDelete: (YourLocation loc) {
store.dispatch(DeleteYourLocationAction(loc));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(S.of(context).youDeletedThisPlace),
action: SnackBarAction(
label: S.of(context).UNDO,
onPressed: () {
store.dispatch(AddYourLocationAction(loc));
})));
},
onToggleSubs: (YourLocation loc) {
store.dispatch(ToggleSubscriptionAction(loc));
},
onTap: (YourLocation loc) {
gotoLocationMap(store, loc, context);
},
onRefresh: (Completer<void> refreshCallback) =>
store.dispatch(FetchYourLocationsAction(refreshCallback)),
yourLocations: store.state.yourLocations,
isLoading: !store.state.isLoaded);
},
builder: (BuildContext context, _ViewModel view) {
final bool hasLocations = view.yourLocations.isNotEmpty;
final String title = hasLocations
? S.of(context).firesInYourPlaces
: S.of(context).firesInTheWorld;
return Scaffold(
key: _scaffoldKey,
// FIXME new?
drawer: MainDrawer(context, ActiveFiresPage.routeName),
appBar: AppBar(
title: Text(title),
leading: IconButton(
icon: const Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState?.openDrawer();
},
),
),
floatingActionButton: SpeedDial(
icon: Icons.add,
activeIcon: Icons.close,
backgroundColor: fires600,
spacing: 12,
children: _fabMiniMenuItemList(context, view.onAdd),
),
bottomNavigationBar: const GlobalFiresBottomStats(),
body: view.isLoading
? const FiresSpinner()
: hasLocations
? RefreshIndicator(
child: _buildSavedLocations(context, view.yourLocations,
view.onDelete, view.onToggleSubs, view.onTap),
onRefresh: () async {
final Completer<void> completer = Completer<void>();
view.onRefresh(completer);
return completer.future;
})
: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RoundedBtn(
icon: Icons.location_searching,
text: S.of(context).addYourCurrentPosition,
onPressed: () => onAddYourLocation(view.onAdd),
backColor: fires600),
const SizedBox(height: 26.0),
RoundedBtn(
icon: Icons.edit_location,
text: S.of(context).addSomePlace,
onPressed: () => onAddOtherLocation(view.onAdd),
backColor: fires600),
])),
);
});
}
void gotoLocationMap(
Store<AppState> store, YourLocation loc, BuildContext context) {
store.dispatch(ShowYourLocationMapAction(loc));
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (BuildContext context) => const GenericMap()));
}
void onAddYourLocation(AddYourLocationFunction onAdd) {
final Future<YourLocation> location = getUserLocation(_scaffoldKey);
_saveLocation(location, onAdd);
}
void onAddOtherLocation(AddYourLocationFunction onAdd) {
final Future<YourLocation> location = openPlacesDialog(_scaffoldKey);
_saveLocation(location, onAdd);
}
void _saveLocation(
Future<YourLocation> location, AddYourLocationFunction onAdd) {
location.then((YourLocation newLocation) {
if (newLocation != YourLocation.noLocation) {
onAdd(newLocation);
}
});
}
}