diff --git a/lib/activeFires.dart b/lib/activeFires.dart index be6d1af..ca80d54 100644 --- a/lib/activeFires.dart +++ b/lib/activeFires.dart @@ -13,19 +13,23 @@ import 'globalFiresBottomStats.dart'; import 'locationUtils.dart'; import 'mainDrawer.dart'; import 'models/appState.dart'; +import 'package:redux/src/store.dart'; import 'placesAutocompleteUtils.dart'; import 'redux/actions.dart'; +@immutable class _ViewModel { final List yourLocations; final AddYourLocationFunction onAdd; final DeleteYourLocationFunction onDelete; final ToggleSubscriptionFunction onToggleSubs; + final OnLocationTapFunction onTap; _ViewModel( {@required this.onAdd, @required this.onDelete, @required this.onToggleSubs, + @required this.onTap, @required this.yourLocations}); @override @@ -36,14 +40,16 @@ class _ViewModel { yourLocations == other.yourLocations && onAdd == other.onAdd && onDelete == other.onDelete && - onToggleSubs == other.onToggleSubs; + onToggleSubs == other.onToggleSubs && + onTap == other.onTap; @override int get hashCode => yourLocations.hashCode ^ onAdd.hashCode ^ onDelete.hashCode ^ - onToggleSubs.hashCode; + onToggleSubs.hashCode ^ + onTap.hashCode; } class ActiveFiresPage extends StatefulWidget { @@ -81,7 +87,7 @@ class _ActiveFiresPageState extends State { _ActiveFiresPageState(); - Widget _buildRow(YourLocation loc, onToggle) { + Widget _buildRow(YourLocation loc, onToggle, onTap) { return new ListTile( dense: true, leading: const Icon(Icons.location_on), @@ -92,44 +98,29 @@ class _ActiveFiresPageState extends State { onPressed: () { loc.subscribed = !loc.subscribed; onToggle(loc); - /* FIXME int i = globals.yourLocations.indexOf(loc); - globals.yourLocations.removeAt(i); - globals.yourLocations.insert(i, loc); - persistYourLocations(); */ - setState(() {}); }), title: new Text(loc.description), onLongPress: () { showSnackMsg(S.of(context).toDeleteThisPlace); }, onTap: () { - showLocationMap(loc); + onTap(loc); }); } - void showLocationMap(YourLocation loc) { - // , VoidCallback onSubs - Navigator.push( - context, - new MaterialPageRoute( - builder: (context) => new GenericMap( - title: loc.description, - location: loc, - operation: MapOperation.view))); - } - void showSnackMsg(String msg) { _scaffoldKey.currentState.showSnackBar(new SnackBar( content: new Text(msg), )); } - Widget _buildSavedLocations(List yl, onDeleted, onToggle) { + Widget _buildSavedLocations( + List yl, onDeleted, onToggle, onTap) { return new ListView.builder( padding: const EdgeInsets.all(16.0), itemCount: yl.length, itemBuilder: (BuildContext _context, int i) { - return _buildItem(yl.elementAt(i), onDeleted, onToggle); + return _buildItem(yl.elementAt(i), onDeleted, onToggle, onTap); }); } @@ -141,7 +132,7 @@ class _ActiveFiresPageState extends State { }); } - Widget _buildItem(YourLocation item, onDelete, onToggle) { + Widget _buildItem(YourLocation item, onDelete, onToggle, onTap) { final ThemeData theme = Theme.of(context); return new Dismissible( key: new ObjectKey(item), @@ -174,7 +165,7 @@ class _ActiveFiresPageState extends State { color: theme.canvasColor, border: new Border( bottom: new BorderSide(color: theme.dividerColor))), - child: _buildRow(item, onToggle))); + child: _buildRow(item, onToggle, onTap))); } @override @@ -185,6 +176,7 @@ class _ActiveFiresPageState extends State { return new _ViewModel( onAdd: (loc) { store.dispatch(new AddYourLocationAction(loc)); + gotoLocationMap(store, loc, context); }, onDelete: (id) { store.dispatch(new DeleteYourLocationAction(id)); @@ -192,6 +184,9 @@ class _ActiveFiresPageState extends State { onToggleSubs: (loc) { store.dispatch(new ToggleSubscriptionAction(loc)); }, + onTap: (loc) { + gotoLocationMap(store, loc, context); + }, yourLocations: store.state.yourLocations); }, builder: (context, view) { @@ -219,8 +214,8 @@ class _ActiveFiresPageState extends State { bottomNavigationBar: new GlobalFiresBottomStats(), body: hasLocations ? new Stack(children: [ - _buildSavedLocations( - view.yourLocations, view.onDelete, view.onToggleSubs), + _buildSavedLocations(view.yourLocations, view.onDelete, + view.onToggleSubs, view.onTap), new FabDialer(_fabMiniMenuItemList(view.onAdd), fires600, new Icon(Icons.add)) ]) @@ -242,6 +237,12 @@ class _ActiveFiresPageState extends State { }); } + void gotoLocationMap(Store store, YourLocation loc, BuildContext context) { + store.dispatch(new ShowYourLocationMapAction(loc)); + Navigator.push( + context, new MaterialPageRoute(builder: (context) => new GenericMap())); + } + void onAddYourLocation(AddYourLocationFunction onAdd) { Future location = getUserLocation(_scaffoldKey); _saveLocation(location, onAdd); @@ -256,17 +257,6 @@ class _ActiveFiresPageState extends State { location.then((newLocation) { if (newLocation != YourLocation.noLocation) { onAdd(newLocation); - /* FIXME - if (globals.yourLocations.contains(newLocation)) { - showSnackMsg(S.of(context).addedThisLocation); - } else - this.setState(() { - globals.yourLocations.add(newLocation); - persistYourLocations(); - new Timer(new Duration(milliseconds: 1000), () { - showLocationMap(newLocation); - }); - }); */ } }); } diff --git a/lib/genericMap.dart b/lib/genericMap.dart index da99af8..8d38c47 100644 --- a/lib/genericMap.dart +++ b/lib/genericMap.dart @@ -3,14 +3,16 @@ import 'dart:core'; import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:fires_flutter/models/basicLocation.dart'; +import 'package:fires_flutter/models/yourLocation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_redux/flutter_redux.dart'; import 'package:http/http.dart' as http; import 'package:just_debounce_it/just_debounce_it.dart'; import 'package:latlong/latlong.dart'; -import 'package:fires_flutter/models/basicLocation.dart'; import 'colors.dart'; import 'customBottomAppBar.dart'; import 'dummyMapPlugin.dart'; @@ -18,47 +20,61 @@ import 'fireMarkType.dart'; import 'fireMarker.dart'; import 'generated/i18n.dart'; import 'globals.dart' as globals; +import 'models/appState.dart'; +import 'models/fireMapState.dart'; +import 'redux/actions.dart'; import 'slider.dart'; -import 'package:fires_flutter/models/yourLocation.dart'; import 'zoomMapPlugin.dart'; -enum MapOperation { view, subscriptionConfirm, unsubscribe } +@immutable +class _ViewModel { + final FireMapState mapState; + final OnSubscribeFunction onSubs; + final OnSubscribeConfirmedFunction onSubsConfirmed; + final OnUnSubscribeFunction onUnSubs; -class GenericMap extends StatefulWidget { - final YourLocation location; - final String title; - final MapOperation operation; - - GenericMap( - {@required this.title, - @required this.location, - @required this.operation}); + _ViewModel( + {@required this.mapState, + @required this.onSubs, + @required this.onSubsConfirmed, + @required this.onUnSubs}); @override - _GenericMapState createState() => - _GenericMapState(title: title, location: location, operation: operation); + bool operator ==(Object other) => + identical(this, other) || + other is _ViewModel && + runtimeType == other.runtimeType && + mapState == other.mapState && + onSubs == other.onSubs && + onSubsConfirmed == other.onSubsConfirmed && + onUnSubs == other.onUnSubs; + + @override + int get hashCode => + mapState.hashCode ^ + onSubs.hashCode ^ + onSubsConfirmed.hashCode ^ + onUnSubs.hashCode; +} + +class GenericMap extends StatefulWidget { + GenericMap(); + + @override + _GenericMapState createState() => _GenericMapState(); } class _GenericMapState extends State { final GlobalKey _scaffoldKey = new GlobalKey(); final FirebaseMessaging _firebaseMessaging = new FirebaseMessaging(); - final YourLocation location; - final String title; int numFires; int kmAround = 100; List fires = []; List falsePos = []; List industries = []; - MapOperation operation; - @override - void initState() { - super.initState(); - updateFireStats(); - } - - void updateFireStats() { + void updateFireStats(YourLocation location) { var url = '${globals.firesApiUrl}fires-in-full/${globals .firesApiKey}/${location.lat}/${location.lon}/$kmAround'; http.read(url).then((result) { @@ -81,145 +97,170 @@ class _GenericMapState extends State { }); } - _GenericMapState( - {@required this.title, - @required this.location, - @required this.operation}); + _GenericMapState(); @override Widget build(BuildContext context) { - print('Build map with operation: $operation'); - MapOptions mapOptions = new MapOptions( - center: new LatLng(this.location.lat, this.location.lon), - plugins: globals.isDevelopment - ? [new ZoomMapPlugin()] - : [new DummyMapPlugin()], - // this works ? - // interactive: false, - zoom: 13.0, - // THIS does not works as expected - // maxZoom: 6.0, - onPositionChanged: (positionCallback) { - // decouple - // print('${positionCallback.center}, ${positionCallback.zoom}'); - }); - var mapController = new MapController(); - // mapController.fitBounds(bounds); - // mapController.center - var fmap = new Opacity( - opacity: operation == MapOperation.subscriptionConfirm ? 0.5 : 1.0, - child: new FlutterMap( - options: mapOptions, - mapController: mapController, - layers: [ - new TileLayerOptions( - maxZoom: 6.0, - subdomains: ['a', 'b', 'c'], - urlTemplate: - 'https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png', - additionalOptions: { - // 'opacity': '0.1', - 'attribution': - '© OpenStreetMap contributors' + // print('Build map with operation: $operation'); + return new StoreConnector( + distinct: false, // FIXME + converter: (store) { + return new _ViewModel( + onSubs: (loc) { + store.dispatch(new SubscribeAction(loc.id)); }, - ), - globals.isDevelopment - ? new ZoomMapPluginOptions() - : new DummyMapPluginOptions(), - new MarkerLayerOptions( - markers: buildMarkers( - this.location, this.fires, this.industries, this.falsePos), - ), - ], - )); - final btnText = operation == MapOperation.view - ? S.of(context).toFiresNotifications - : operation == MapOperation.subscriptionConfirm - ? S.of(context).confirm - : S.of(context).unsubscribe; - final btnIcon = operation == MapOperation.view - ? Icons.notifications_active - : operation == MapOperation.subscriptionConfirm - ? Icons.check - : Icons.notifications_off; - return new Scaffold( - key: _scaffoldKey, - appBar: new AppBar( - title: new Text(title), - ), - floatingActionButton: FloatingActionButton.extended( - onPressed: () { - setState(() { - switch (operation) { - case MapOperation.view: - operation = MapOperation.subscriptionConfirm; - break; - case MapOperation.subscriptionConfirm: - // IOS specific - _firebaseMessaging.requestNotificationPermissions(); - operation = MapOperation.unsubscribe; - break; - case MapOperation.unsubscribe: - operation = MapOperation.view; - break; - } - }); - }, - // https://github.com/flutter/flutter/issues/17583 - heroTag: "firesmap" + location.id.toHexString(), - icon: new Icon(btnIcon, color: fires600), - label: new Text( - btnText, - style: const TextStyle(color: fires600), - ), - backgroundColor: Colors.white, - ), - floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, - bottomNavigationBar: new CustomBottomAppBar( - fabLocation: FloatingActionButtonLocation.centerFloat, - showNotch: false, - color: fires100, - // height: 170.0, - mainAxisAlignment: MainAxisAlignment.center, - actions: listWithoutNulls([ - operation == MapOperation.subscriptionConfirm || numFires == null - ? null - : new Text(numFires > 0 - ? S.of(context).firesAroundThisArea( - numFires.toString(), kmAround.toString()) - : S - .of(context) - .noFiresAroundThisArea(kmAround.toString())), - SizedBox(width: 10.0) - ])), - body: LayoutBuilder( - builder: (context, constraints) => - Stack(fit: StackFit.expand, children: [ - // Material(color: Colors.yellowAccent), - fmap, - Positioned( - top: constraints.maxHeight - 200, - right: 10.0, - left: 10.0, - child: new CenteredRow( - // Fit sample: - // https://github.com/apptreesoftware/flutter_map/blob/master/flutter_map_example/lib/pages/map_controller.dart - children: operation == MapOperation.subscriptionConfirm - ? [ - new FireDistanceSlider( - initialValue: kmAround, - onSlide: (distance) { - setState(() { - kmAround = distance; - Debounce.seconds(1, updateFireStats); - }); - }) - ] - : [], - ), - ) - ]), - )); + onSubsConfirmed: (loc) { + store.dispatch(new SubscribeAction(loc.id)); + }, + onUnSubs: (loc) { + store.dispatch(new UnSubscribeAction(loc.id)); + }, + mapState: store.state.fireMapState); + }, + builder: (context, view) { + YourLocation location = view.mapState.yourLocation; + MapOptions mapOptions = new MapOptions( + center: new LatLng(location.lat, location.lon), + plugins: globals.isDevelopment + ? [new ZoomMapPlugin()] + : [new DummyMapPlugin()], + // this works ? + // interactive: false, + zoom: 13.0, + // THIS does not works as expected + // maxZoom: 6.0, + onPositionChanged: (positionCallback) { + // decouple + // print('${positionCallback.center}, ${positionCallback.zoom}'); + }); + var mapController = new MapController(); + // mapController.fitBounds(bounds); + // mapController.center + + FireMapStatus operation = view.mapState.status; + final btnText = operation == FireMapStatus.view + ? S.of(context).toFiresNotifications + : operation == FireMapStatus.subscriptionConfirm + ? S.of(context).confirm + : S.of(context).unsubscribe; + final btnIcon = operation == FireMapStatus.view + ? Icons.notifications_active + : operation == FireMapStatus.subscriptionConfirm + ? Icons.check + : Icons.notifications_off; + updateFireStats(location); + return new Scaffold( + key: _scaffoldKey, + appBar: new AppBar( + title: new Text(location.description), + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () { + + switch (operation) { + case FireMapStatus.view: + view.onSubs(location); + break; + case FireMapStatus.subscriptionConfirm: + view.onSubsConfirmed(location); + // IOS specific + _firebaseMessaging.requestNotificationPermissions(); + operation = FireMapStatus.unsubscribe; + break; + case FireMapStatus.unsubscribe: + view.onUnSubs(location); + operation = FireMapStatus.view; + break; + } + + }, + // https://github.com/flutter/flutter/issues/17583 + heroTag: "firesmap" + location.id.toHexString(), + icon: new Icon(btnIcon, color: fires600), + label: new Text( + btnText, + style: const TextStyle(color: fires600), + ), + backgroundColor: Colors.white, + ), + floatingActionButtonLocation: + FloatingActionButtonLocation.centerFloat, + bottomNavigationBar: new CustomBottomAppBar( + fabLocation: FloatingActionButtonLocation.centerFloat, + showNotch: false, + color: fires100, + // height: 170.0, + mainAxisAlignment: MainAxisAlignment.center, + actions: listWithoutNulls([ + operation == FireMapStatus.subscriptionConfirm || + numFires == null + ? null + : new Text(numFires > 0 + ? S.of(context).firesAroundThisArea( + numFires.toString(), kmAround.toString()) + : S + .of(context) + .noFiresAroundThisArea(kmAround.toString())), + SizedBox(width: 10.0) + ])), + body: LayoutBuilder( + builder: (context, constraints) => + Stack(fit: StackFit.expand, children: [ + // Material(color: Colors.yellowAccent), + new Opacity( + opacity: + operation == FireMapStatus.subscriptionConfirm + ? 0.5 + : 1.0, + child: new FlutterMap( + options: mapOptions, + mapController: mapController, + layers: [ + new TileLayerOptions( + maxZoom: 6.0, + subdomains: ['a', 'b', 'c'], + urlTemplate: + 'https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png', + additionalOptions: { + // 'opacity': '0.1', + 'attribution': + '© OpenStreetMap contributors' + }, + ), + globals.isDevelopment + ? new ZoomMapPluginOptions() + : new DummyMapPluginOptions(), + new MarkerLayerOptions( + markers: buildMarkers(location, this.fires, + this.industries, this.falsePos), + ), + ], + )), + Positioned( + top: constraints.maxHeight - 200, + right: 10.0, + left: 10.0, + child: new CenteredRow( + // Fit sample: + // https://github.com/apptreesoftware/flutter_map/blob/master/flutter_map_example/lib/pages/map_controller.dart + children: operation == + FireMapStatus.subscriptionConfirm + ? [ + new FireDistanceSlider( + initialValue: kmAround, + onSlide: (distance) { + setState(() { + kmAround = distance; + Debounce.seconds(1, updateFireStats); + }); + }) + ] + : [], + ), + ) + ]), + )); + }); } List buildMarkers(YourLocation yourLocation, List fires, diff --git a/lib/mainCommon.dart b/lib/mainCommon.dart index bc5b6ce..a629ef8 100644 --- a/lib/mainCommon.dart +++ b/lib/mainCommon.dart @@ -12,7 +12,6 @@ import 'models/firesApi.dart'; import 'redux/fetchDataMiddleware.dart'; import 'redux/reducers.dart'; import 'package:fires_flutter/models/yourLocationPersist.dart'; -import 'package:fires_flutter/models/yourLocation.dart'; Future> loadSecrets() async { return await SecretLoader(secretPath: 'assets/private-settings.json').load(); diff --git a/lib/models/appState.dart b/lib/models/appState.dart index 796de57..a2a2d4c 100644 --- a/lib/models/appState.dart +++ b/lib/models/appState.dart @@ -78,7 +78,12 @@ class AppState extends Object with _$AppStateSerializerMixin { typedef void AddYourLocationFunction(YourLocation loc); typedef void DeleteYourLocationFunction(ObjectId id); -typedef void UpdateYourLocationFunction(ObjectId id, YourLocation loc); typedef void ToggleSubscriptionFunction(YourLocation loc); -typedef void SubscribeFunction(ObjectId id); -typedef void UnSubscribeFunction(ObjectId id); + +typedef void OnLocationTapFunction(YourLocation loc); +typedef void OnSubscribeFunction(YourLocation loc); +typedef void OnUnSubscribeFunction(YourLocation loc); +typedef void OnSubscribeConfirmedFunction(YourLocation loc); + +// unused +// typedef void UpdateYourLocationFunction(ObjectId id, YourLocation loc); diff --git a/lib/models/fireMapState.dart b/lib/models/fireMapState.dart index c32b8ee..117076c 100644 --- a/lib/models/fireMapState.dart +++ b/lib/models/fireMapState.dart @@ -1,27 +1,33 @@ import 'package:meta/meta.dart'; - -enum FireMapOperation { view, subscriptionConfirm, unsubscribe } +import 'package:fires_flutter/models/yourLocation.dart'; +import 'package:redux/redux.dart'; +enum FireMapStatus { view, subscriptionConfirm, unsubscribe } @immutable class FireMapState { - final FireMapOperation currentOperation; + final FireMapStatus status; + final YourLocation yourLocation; - const FireMapState.initial(): this.currentOperation = FireMapOperation.view; + const FireMapState.initial(): this.status = FireMapStatus.view, this.yourLocation = null; - FireMapState({this.currentOperation: FireMapOperation.view}); + FireMapState({this.status: FireMapStatus.view, this.yourLocation}); + + FireMapState copyWith({FireMapStatus status, YourLocation yourLocation}) { + return new FireMapState( + yourLocation: yourLocation ?? this.yourLocation, + status: status ?? this.status); + } @override bool operator ==(Object other) => identical(this, other) || other is FireMapState && runtimeType == other.runtimeType && - currentOperation == other.currentOperation; + status == other.status && + yourLocation == other.yourLocation; @override - int get hashCode => currentOperation.hashCode; - - FireMapState copyWith({FireMapOperation currentOperation}) { - return new FireMapState( - currentOperation: currentOperation ?? this.currentOperation); - } + int get hashCode => + status.hashCode ^ + yourLocation.hashCode; } diff --git a/lib/models/firesApi.dart b/lib/models/firesApi.dart index 4436c89..42803cd 100644 --- a/lib/models/firesApi.dart +++ b/lib/models/firesApi.dart @@ -51,7 +51,6 @@ class FiresApi { var el = dataSubscriptions[i]; var lat = el['location']['lat']; var lon = el['location']['lon']; - var id = el['_id']['_str']; subscribed.add(new YourLocation( id: ObjectId.fromHexString(el['_id']['_str']), lat: lat, diff --git a/lib/redux/appReducer.dart b/lib/redux/appReducer.dart index 6390898..0e423a6 100644 --- a/lib/redux/appReducer.dart +++ b/lib/redux/appReducer.dart @@ -1,5 +1,4 @@ import 'actions.dart'; -import '../models/user.dart'; import '../models/appState.dart'; AppState appReducer(AppState state, action) { diff --git a/lib/redux/fireMapReducer.dart b/lib/redux/fireMapReducer.dart index b2119bb..59f19d4 100644 --- a/lib/redux/fireMapReducer.dart +++ b/lib/redux/fireMapReducer.dart @@ -1,5 +1,20 @@ -import '../models/fireMapState.dart'; +import 'package:redux/redux.dart'; -FireMapState fireMapReducer(FireMapState state, action) { - return state; -} \ No newline at end of file +import '../models/fireMapState.dart'; +import 'actions.dart'; + +final fireMapReducer = combineReducers([ + new TypedReducer( + _showYourLocationMap), +]); + +FireMapState _showYourLocationMap( + FireMapState state, ShowYourLocationMapAction action) { + if (action.loc.subscribed) { + return state.copyWith( + status: action.loc.subscribed + ? FireMapStatus.unsubscribe + : FireMapStatus.view, + yourLocation: action.loc); + } +} diff --git a/lib/redux/yourLocationActions.dart b/lib/redux/yourLocationActions.dart index 747f1c6..437a3be 100644 --- a/lib/redux/yourLocationActions.dart +++ b/lib/redux/yourLocationActions.dart @@ -2,7 +2,6 @@ import 'package:bson_objectid/bson_objectid.dart'; import 'package:fires_flutter/models/yourLocation.dart'; - abstract class YourLocationActions {} class AddYourLocationAction extends YourLocationActions { @@ -17,6 +16,12 @@ class AddedYourLocationAction extends YourLocationActions { AddedYourLocationAction(this.loc); } +class ShowYourLocationMapAction extends YourLocationActions { + YourLocation loc; + + ShowYourLocationMapAction(this.loc); +} + class DeleteYourLocationAction extends YourLocationActions { ObjectId id;