mirror of
https://github.com/flutter/samples.git
synced 2026-04-02 17:52:56 +00:00
Place tracker/maintenance (#519)
* format place_tracker README * update sentence in README * sort Dart members, remove unnecessary type declarations * Run place_tracker on Android and iOS, update project files * add link to developer preview caveat * grammar * update MAINTENANCE
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
|
||||
enum PlaceCategory {
|
||||
favorite,
|
||||
visited,
|
||||
wantToGo,
|
||||
}
|
||||
|
||||
class Place {
|
||||
final String id;
|
||||
final LatLng latLng;
|
||||
final String name;
|
||||
final PlaceCategory category;
|
||||
final String description;
|
||||
final int starRating;
|
||||
|
||||
const Place({
|
||||
@required this.id,
|
||||
@required this.latLng,
|
||||
@@ -21,13 +22,6 @@ class Place {
|
||||
assert(category != null),
|
||||
assert(starRating != null && starRating >= 0 && starRating <= 5);
|
||||
|
||||
final String id;
|
||||
final LatLng latLng;
|
||||
final String name;
|
||||
final PlaceCategory category;
|
||||
final String description;
|
||||
final int starRating;
|
||||
|
||||
double get latitude => latLng.latitude;
|
||||
|
||||
double get longitude => latLng.longitude;
|
||||
@@ -50,3 +44,9 @@ class Place {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum PlaceCategory {
|
||||
favorite,
|
||||
visited,
|
||||
wantToGo,
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ import 'place.dart';
|
||||
import 'stub_data.dart';
|
||||
|
||||
class PlaceDetails extends StatefulWidget {
|
||||
final Place place;
|
||||
final ValueChanged<Place> onChanged;
|
||||
|
||||
const PlaceDetails({
|
||||
@required this.place,
|
||||
@required this.onChanged,
|
||||
@@ -15,9 +18,6 @@ class PlaceDetails extends StatefulWidget {
|
||||
assert(onChanged != null),
|
||||
super(key: key);
|
||||
|
||||
final Place place;
|
||||
final ValueChanged<Place> onChanged;
|
||||
|
||||
@override
|
||||
PlaceDetailsState createState() => PlaceDetailsState();
|
||||
}
|
||||
@@ -26,10 +26,37 @@ class PlaceDetailsState extends State<PlaceDetails> {
|
||||
Place _place;
|
||||
GoogleMapController _mapController;
|
||||
final Set<Marker> _markers = {};
|
||||
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _descriptionController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('${_place.name}'),
|
||||
backgroundColor: Colors.green[700],
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.save, size: 30.0),
|
||||
onPressed: () {
|
||||
widget.onChanged(_place);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: GestureDetector(
|
||||
onTap: () {
|
||||
FocusScope.of(context).requestFocus(FocusNode());
|
||||
},
|
||||
child: _detailsBody(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_place = widget.place;
|
||||
@@ -38,20 +65,10 @@ class PlaceDetailsState extends State<PlaceDetails> {
|
||||
return super.initState();
|
||||
}
|
||||
|
||||
void _onMapCreated(GoogleMapController controller) {
|
||||
_mapController = controller;
|
||||
setState(() {
|
||||
_markers.add(Marker(
|
||||
markerId: MarkerId(_place.latLng.toString()),
|
||||
position: _place.latLng,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
Widget _detailsBody() {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(24.0, 12.0, 24.0, 12.0),
|
||||
children: <Widget>[
|
||||
children: [
|
||||
_NameTextField(
|
||||
controller: _nameController,
|
||||
onChanged: (value) {
|
||||
@@ -87,68 +104,21 @@ class PlaceDetailsState extends State<PlaceDetails> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('${_place.name}'),
|
||||
backgroundColor: Colors.green[700],
|
||||
actions: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.save, size: 30.0),
|
||||
onPressed: () {
|
||||
widget.onChanged(_place);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: GestureDetector(
|
||||
onTap: () {
|
||||
FocusScope.of(context).requestFocus(FocusNode());
|
||||
},
|
||||
child: _detailsBody(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NameTextField extends StatelessWidget {
|
||||
const _NameTextField({
|
||||
@required this.controller,
|
||||
@required this.onChanged,
|
||||
Key key,
|
||||
}) : assert(controller != null),
|
||||
assert(onChanged != null),
|
||||
super(key: key);
|
||||
|
||||
final TextEditingController controller;
|
||||
final ValueChanged<String> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 16.0),
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Name',
|
||||
labelStyle: TextStyle(fontSize: 18.0),
|
||||
),
|
||||
style: const TextStyle(fontSize: 20.0, color: Colors.black87),
|
||||
autocorrect: true,
|
||||
controller: controller,
|
||||
onChanged: (value) {
|
||||
onChanged(value);
|
||||
},
|
||||
),
|
||||
);
|
||||
void _onMapCreated(GoogleMapController controller) {
|
||||
_mapController = controller;
|
||||
setState(() {
|
||||
_markers.add(Marker(
|
||||
markerId: MarkerId(_place.latLng.toString()),
|
||||
position: _place.latLng,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _DescriptionTextField extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
|
||||
final ValueChanged<String> onChanged;
|
||||
const _DescriptionTextField({
|
||||
@required this.controller,
|
||||
@required this.onChanged,
|
||||
@@ -157,9 +127,6 @@ class _DescriptionTextField extends StatelessWidget {
|
||||
assert(onChanged != null),
|
||||
super(key: key);
|
||||
|
||||
final TextEditingController controller;
|
||||
final ValueChanged<String> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
@@ -181,38 +148,12 @@ class _DescriptionTextField extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _StarBar extends StatelessWidget {
|
||||
const _StarBar({
|
||||
@required this.rating,
|
||||
@required this.onChanged,
|
||||
Key key,
|
||||
}) : assert(rating != null && rating >= 0 && rating <= maxStars),
|
||||
assert(onChanged != null),
|
||||
super(key: key);
|
||||
|
||||
static const int maxStars = 5;
|
||||
final int rating;
|
||||
final ValueChanged<int> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(maxStars, (index) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.star),
|
||||
iconSize: 40.0,
|
||||
color: rating > index ? Colors.amber : Colors.grey[400],
|
||||
onPressed: () {
|
||||
onChanged(index + 1);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Map extends StatelessWidget {
|
||||
final LatLng center;
|
||||
|
||||
final GoogleMapController mapController;
|
||||
final ArgumentCallback<GoogleMapController> onMapCreated;
|
||||
final Set<Marker> markers;
|
||||
const _Map({
|
||||
@required this.center,
|
||||
@required this.mapController,
|
||||
@@ -223,24 +164,19 @@ class _Map extends StatelessWidget {
|
||||
assert(onMapCreated != null),
|
||||
super(key: key);
|
||||
|
||||
final LatLng center;
|
||||
final GoogleMapController mapController;
|
||||
final ArgumentCallback<GoogleMapController> onMapCreated;
|
||||
final Set<Marker> markers;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
elevation: 4.0,
|
||||
elevation: 4,
|
||||
child: SizedBox(
|
||||
width: 340.0,
|
||||
height: 240.0,
|
||||
width: 340,
|
||||
height: 240,
|
||||
child: GoogleMap(
|
||||
onMapCreated: onMapCreated,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: center,
|
||||
zoom: 16.0,
|
||||
zoom: 16,
|
||||
),
|
||||
markers: markers,
|
||||
zoomGesturesEnabled: false,
|
||||
@@ -253,78 +189,55 @@ class _Map extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _NameTextField extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
|
||||
final ValueChanged<String> onChanged;
|
||||
const _NameTextField({
|
||||
@required this.controller,
|
||||
@required this.onChanged,
|
||||
Key key,
|
||||
}) : assert(controller != null),
|
||||
assert(onChanged != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 0, 0, 16),
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Name',
|
||||
labelStyle: TextStyle(fontSize: 18),
|
||||
),
|
||||
style: const TextStyle(fontSize: 20, color: Colors.black87),
|
||||
autocorrect: true,
|
||||
controller: controller,
|
||||
onChanged: (value) {
|
||||
onChanged(value);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Reviews extends StatelessWidget {
|
||||
const _Reviews({
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
Widget _buildSingleReview(String reviewText) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 80.0,
|
||||
height: 80.0,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(40.0),
|
||||
border: Border.all(
|
||||
width: 3.0,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
Text(
|
||||
'5',
|
||||
style: TextStyle(
|
||||
fontSize: 24.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.star,
|
||||
color: Colors.amber,
|
||||
size: 36.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16.0),
|
||||
Expanded(
|
||||
child: Text(
|
||||
reviewText,
|
||||
style: const TextStyle(fontSize: 20.0, color: Colors.black87),
|
||||
maxLines: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(
|
||||
height: 8.0,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 8.0),
|
||||
padding: EdgeInsets.fromLTRB(0, 12, 0, 8),
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Text(
|
||||
'Reviews',
|
||||
style: TextStyle(
|
||||
fontSize: 24.0,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
decoration: TextDecoration.underline,
|
||||
color: Colors.black87,
|
||||
@@ -340,4 +253,90 @@ class _Reviews extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSingleReview(String reviewText) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
border: Border.all(
|
||||
width: 3,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
Text(
|
||||
'5',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.star,
|
||||
color: Colors.amber,
|
||||
size: 36,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
reviewText,
|
||||
style: const TextStyle(fontSize: 20, color: Colors.black87),
|
||||
maxLines: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(
|
||||
height: 8,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StarBar extends StatelessWidget {
|
||||
static const int maxStars = 5;
|
||||
|
||||
final int rating;
|
||||
final ValueChanged<int> onChanged;
|
||||
const _StarBar({
|
||||
@required this.rating,
|
||||
@required this.onChanged,
|
||||
Key key,
|
||||
}) : assert(rating != null && rating >= 0 && rating <= maxStars),
|
||||
assert(onChanged != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(maxStars, (index) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.star),
|
||||
iconSize: 40,
|
||||
color: rating > index ? Colors.amber : Colors.grey[400],
|
||||
onPressed: () {
|
||||
onChanged(index + 1);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,27 +15,11 @@ class PlaceList extends StatefulWidget {
|
||||
class PlaceListState extends State<PlaceList> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
void _onCategoryChanged(PlaceCategory newCategory) {
|
||||
_scrollController.jumpTo(0.0);
|
||||
Provider.of<AppState>(context, listen: false)
|
||||
.setSelectedCategory(newCategory);
|
||||
}
|
||||
|
||||
void _onPlaceChanged(Place value) {
|
||||
// Replace the place with the modified version.
|
||||
final newPlaces =
|
||||
List<Place>.from(Provider.of<AppState>(context, listen: false).places);
|
||||
final index = newPlaces.indexWhere((place) => place.id == value.id);
|
||||
newPlaces[index] = value;
|
||||
|
||||
Provider.of<AppState>(context, listen: false).setPlaces(newPlaces);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var state = Provider.of<AppState>(context);
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
children: [
|
||||
_ListCategoryButtonBar(
|
||||
selectedCategory: state.selectedCategory,
|
||||
onCategoryChanged: (value) => _onCategoryChanged(value),
|
||||
@@ -57,113 +41,29 @@ class PlaceListState extends State<PlaceList> {
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PlaceListTile extends StatelessWidget {
|
||||
const _PlaceListTile({
|
||||
Key key,
|
||||
@required this.place,
|
||||
@required this.onPlaceChanged,
|
||||
}) : assert(place != null),
|
||||
assert(onPlaceChanged != null),
|
||||
super(key: key);
|
||||
|
||||
final Place place;
|
||||
final ValueChanged<Place> onPlaceChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () => Navigator.push<void>(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return PlaceDetails(
|
||||
place: place,
|
||||
onChanged: (value) => onPlaceChanged(value),
|
||||
);
|
||||
}),
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(top: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
place.name,
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20.0,
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
Row(
|
||||
children: List.generate(5, (index) {
|
||||
return Icon(
|
||||
Icons.star,
|
||||
size: 28.0,
|
||||
color: place.starRating > index
|
||||
? Colors.amber
|
||||
: Colors.grey[400],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
Text(
|
||||
place.description ?? '',
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
maxLines: 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 16.0),
|
||||
Divider(
|
||||
height: 2.0,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
void _onCategoryChanged(PlaceCategory newCategory) {
|
||||
_scrollController.jumpTo(0.0);
|
||||
Provider.of<AppState>(context, listen: false)
|
||||
.setSelectedCategory(newCategory);
|
||||
}
|
||||
}
|
||||
|
||||
class _ListCategoryButtonBar extends StatelessWidget {
|
||||
const _ListCategoryButtonBar({
|
||||
Key key,
|
||||
@required this.selectedCategory,
|
||||
@required this.onCategoryChanged,
|
||||
}) : assert(selectedCategory != null),
|
||||
assert(onCategoryChanged != null),
|
||||
super(key: key);
|
||||
void _onPlaceChanged(Place value) {
|
||||
// Replace the place with the modified version.
|
||||
final newPlaces =
|
||||
List<Place>.from(Provider.of<AppState>(context, listen: false).places);
|
||||
final index = newPlaces.indexWhere((place) => place.id == value.id);
|
||||
newPlaces[index] = value;
|
||||
|
||||
final PlaceCategory selectedCategory;
|
||||
final ValueChanged<PlaceCategory> onCategoryChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
_CategoryButton(
|
||||
category: PlaceCategory.favorite,
|
||||
selected: selectedCategory == PlaceCategory.favorite,
|
||||
onCategoryChanged: onCategoryChanged,
|
||||
),
|
||||
_CategoryButton(
|
||||
category: PlaceCategory.visited,
|
||||
selected: selectedCategory == PlaceCategory.visited,
|
||||
onCategoryChanged: onCategoryChanged,
|
||||
),
|
||||
_CategoryButton(
|
||||
category: PlaceCategory.wantToGo,
|
||||
selected: selectedCategory == PlaceCategory.wantToGo,
|
||||
onCategoryChanged: onCategoryChanged,
|
||||
),
|
||||
],
|
||||
);
|
||||
Provider.of<AppState>(context, listen: false).setPlaces(newPlaces);
|
||||
}
|
||||
}
|
||||
|
||||
class _CategoryButton extends StatelessWidget {
|
||||
final PlaceCategory category;
|
||||
|
||||
final bool selected;
|
||||
final ValueChanged<PlaceCategory> onCategoryChanged;
|
||||
const _CategoryButton({
|
||||
Key key,
|
||||
@required this.category,
|
||||
@@ -173,10 +73,6 @@ class _CategoryButton extends StatelessWidget {
|
||||
assert(selected != null),
|
||||
super(key: key);
|
||||
|
||||
final PlaceCategory category;
|
||||
final bool selected;
|
||||
final ValueChanged<PlaceCategory> onCategoryChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String _buttonText;
|
||||
@@ -216,3 +112,107 @@ class _CategoryButton extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ListCategoryButtonBar extends StatelessWidget {
|
||||
final PlaceCategory selectedCategory;
|
||||
|
||||
final ValueChanged<PlaceCategory> onCategoryChanged;
|
||||
const _ListCategoryButtonBar({
|
||||
Key key,
|
||||
@required this.selectedCategory,
|
||||
@required this.onCategoryChanged,
|
||||
}) : assert(selectedCategory != null),
|
||||
assert(onCategoryChanged != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_CategoryButton(
|
||||
category: PlaceCategory.favorite,
|
||||
selected: selectedCategory == PlaceCategory.favorite,
|
||||
onCategoryChanged: onCategoryChanged,
|
||||
),
|
||||
_CategoryButton(
|
||||
category: PlaceCategory.visited,
|
||||
selected: selectedCategory == PlaceCategory.visited,
|
||||
onCategoryChanged: onCategoryChanged,
|
||||
),
|
||||
_CategoryButton(
|
||||
category: PlaceCategory.wantToGo,
|
||||
selected: selectedCategory == PlaceCategory.wantToGo,
|
||||
onCategoryChanged: onCategoryChanged,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PlaceListTile extends StatelessWidget {
|
||||
final Place place;
|
||||
|
||||
final ValueChanged<Place> onPlaceChanged;
|
||||
const _PlaceListTile({
|
||||
Key key,
|
||||
@required this.place,
|
||||
@required this.onPlaceChanged,
|
||||
}) : assert(place != null),
|
||||
assert(onPlaceChanged != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () => Navigator.push<void>(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return PlaceDetails(
|
||||
place: place,
|
||||
onChanged: (value) => onPlaceChanged(value),
|
||||
);
|
||||
}),
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(top: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
place.name,
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20.0,
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
Row(
|
||||
children: List.generate(5, (index) {
|
||||
return Icon(
|
||||
Icons.star,
|
||||
size: 28.0,
|
||||
color: place.starRating > index
|
||||
? Colors.amber
|
||||
: Colors.grey[400],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
Text(
|
||||
place.description ?? '',
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
maxLines: 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 16.0),
|
||||
Divider(
|
||||
height: 2.0,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,41 +11,55 @@ import 'place.dart';
|
||||
import 'place_details.dart';
|
||||
import 'place_tracker_app.dart';
|
||||
|
||||
class MapConfiguration {
|
||||
final List<Place> places;
|
||||
|
||||
final PlaceCategory selectedCategory;
|
||||
const MapConfiguration({
|
||||
@required this.places,
|
||||
@required this.selectedCategory,
|
||||
}) : assert(places != null),
|
||||
assert(selectedCategory != null);
|
||||
|
||||
@override
|
||||
int get hashCode => places.hashCode ^ selectedCategory.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return other is MapConfiguration &&
|
||||
other.places == places &&
|
||||
other.selectedCategory == selectedCategory;
|
||||
}
|
||||
|
||||
static MapConfiguration of(AppState appState) {
|
||||
return MapConfiguration(
|
||||
places: appState.places,
|
||||
selectedCategory: appState.selectedCategory,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaceMap extends StatefulWidget {
|
||||
final LatLng center;
|
||||
|
||||
const PlaceMap({
|
||||
Key key,
|
||||
this.center,
|
||||
}) : super(key: key);
|
||||
|
||||
final LatLng center;
|
||||
|
||||
@override
|
||||
PlaceMapState createState() => PlaceMapState();
|
||||
}
|
||||
|
||||
class PlaceMapState extends State<PlaceMap> {
|
||||
static Future<BitmapDescriptor> _getPlaceMarkerIcon(
|
||||
BuildContext context, PlaceCategory category) async {
|
||||
switch (category) {
|
||||
case PlaceCategory.favorite:
|
||||
return BitmapDescriptor.fromAssetImage(
|
||||
createLocalImageConfiguration(context), 'assets/heart.png');
|
||||
break;
|
||||
case PlaceCategory.visited:
|
||||
return BitmapDescriptor.fromAssetImage(
|
||||
createLocalImageConfiguration(context), 'assets/visited.png');
|
||||
break;
|
||||
case PlaceCategory.wantToGo:
|
||||
default:
|
||||
return BitmapDescriptor.defaultMarker;
|
||||
}
|
||||
}
|
||||
|
||||
static List<Place> _getPlacesForCategory(
|
||||
PlaceCategory category, List<Place> places) {
|
||||
return places.where((place) => place.category == category).toList();
|
||||
}
|
||||
|
||||
Completer<GoogleMapController> mapController = Completer();
|
||||
|
||||
MapType _currentMapType = MapType.normal;
|
||||
@@ -60,6 +74,50 @@ class PlaceMapState extends State<PlaceMap> {
|
||||
|
||||
MapConfiguration _configuration;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_maybeUpdateMapConfiguration();
|
||||
var state = Provider.of<AppState>(context);
|
||||
|
||||
return Builder(builder: (context) {
|
||||
// We need this additional builder here so that we can pass its context to
|
||||
// _AddPlaceButtonBar's onSavePressed callback. This callback shows a
|
||||
// SnackBar and to do this, we need a build context that has Scaffold as
|
||||
// an ancestor.
|
||||
return Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
GoogleMap(
|
||||
onMapCreated: onMapCreated,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: widget.center,
|
||||
zoom: 11.0,
|
||||
),
|
||||
mapType: _currentMapType,
|
||||
markers: _markers,
|
||||
onCameraMove: (position) => _lastMapPosition = position.target,
|
||||
),
|
||||
_CategoryButtonBar(
|
||||
selectedPlaceCategory: state.selectedCategory,
|
||||
visible: _pendingMarker == null,
|
||||
onChanged: _switchSelectedCategory,
|
||||
),
|
||||
_AddPlaceButtonBar(
|
||||
visible: _pendingMarker != null,
|
||||
onSavePressed: () => _confirmAddPlace(context),
|
||||
onCancelPressed: _cancelAddPlace,
|
||||
),
|
||||
_MapFabs(
|
||||
visible: _pendingMarker == null,
|
||||
onAddPlacePressed: _onAddPlacePressed,
|
||||
onToggleMapTypePressed: _onToggleMapTypePressed,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> onMapCreated(GoogleMapController controller) async {
|
||||
mapController.complete(controller);
|
||||
_lastMapPosition = widget.center;
|
||||
@@ -83,152 +141,13 @@ class PlaceMapState extends State<PlaceMap> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<Marker> _createPlaceMarker(BuildContext context, Place place) async {
|
||||
final marker = Marker(
|
||||
markerId: MarkerId(place.latLng.toString()),
|
||||
position: place.latLng,
|
||||
infoWindow: InfoWindow(
|
||||
title: place.name,
|
||||
snippet: '${place.starRating} Star Rating',
|
||||
onTap: () => _pushPlaceDetailsScreen(place),
|
||||
),
|
||||
icon: await _getPlaceMarkerIcon(context, place.category),
|
||||
visible: place.category ==
|
||||
Provider.of<AppState>(context, listen: false).selectedCategory,
|
||||
);
|
||||
_markedPlaces[marker] = place;
|
||||
return marker;
|
||||
}
|
||||
|
||||
void _pushPlaceDetailsScreen(Place place) {
|
||||
assert(place != null);
|
||||
|
||||
Navigator.push<void>(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return PlaceDetails(
|
||||
place: place,
|
||||
onChanged: (value) => _onPlaceChanged(value),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
void _onPlaceChanged(Place value) {
|
||||
// Replace the place with the modified version.
|
||||
final newPlaces =
|
||||
List<Place>.from(Provider.of<AppState>(context, listen: false).places);
|
||||
final index = newPlaces.indexWhere((place) => place.id == value.id);
|
||||
newPlaces[index] = value;
|
||||
|
||||
_updateExistingPlaceMarker(place: value);
|
||||
|
||||
// Manually update our map configuration here since our map is already
|
||||
// updated with the new marker. Otherwise, the map would be reconfigured
|
||||
// in the main build method due to a modified AppState.
|
||||
_configuration = MapConfiguration(
|
||||
places: newPlaces,
|
||||
selectedCategory:
|
||||
Provider.of<AppState>(context, listen: false).selectedCategory,
|
||||
);
|
||||
|
||||
Provider.of<AppState>(context, listen: false).setPlaces(newPlaces);
|
||||
}
|
||||
|
||||
void _updateExistingPlaceMarker({@required Place place}) {
|
||||
var marker = _markedPlaces.keys
|
||||
.singleWhere((value) => _markedPlaces[value].id == place.id);
|
||||
|
||||
setState(() {
|
||||
final updatedMarker = marker.copyWith(
|
||||
infoWindowParam: InfoWindow(
|
||||
title: place.name,
|
||||
snippet:
|
||||
place.starRating != 0 ? '${place.starRating} Star Rating' : null,
|
||||
),
|
||||
);
|
||||
_updateMarker(marker: marker, updatedMarker: updatedMarker, place: place);
|
||||
});
|
||||
}
|
||||
|
||||
void _updateMarker({
|
||||
@required Marker marker,
|
||||
@required Marker updatedMarker,
|
||||
@required Place place,
|
||||
}) {
|
||||
_markers.remove(marker);
|
||||
_markedPlaces.remove(marker);
|
||||
|
||||
_markers.add(updatedMarker);
|
||||
_markedPlaces[updatedMarker] = place;
|
||||
}
|
||||
|
||||
Future<void> _switchSelectedCategory(PlaceCategory category) async {
|
||||
Provider.of<AppState>(context, listen: false).setSelectedCategory(category);
|
||||
await _showPlacesForSelectedCategory(category);
|
||||
}
|
||||
|
||||
Future<void> _showPlacesForSelectedCategory(PlaceCategory category) async {
|
||||
setState(() {
|
||||
for (var marker in List.of(_markedPlaces.keys)) {
|
||||
final place = _markedPlaces[marker];
|
||||
final updatedMarker = marker.copyWith(
|
||||
visibleParam: place.category == category,
|
||||
);
|
||||
|
||||
_updateMarker(
|
||||
marker: marker,
|
||||
updatedMarker: updatedMarker,
|
||||
place: place,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await _zoomToFitPlaces(_getPlacesForCategory(
|
||||
category,
|
||||
_markedPlaces.values.toList(),
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> _zoomToFitPlaces(List<Place> places) async {
|
||||
var controller = await mapController.future;
|
||||
|
||||
// Default min/max values to latitude and longitude of center.
|
||||
var minLat = widget.center.latitude;
|
||||
var maxLat = widget.center.latitude;
|
||||
var minLong = widget.center.longitude;
|
||||
var maxLong = widget.center.longitude;
|
||||
|
||||
for (var place in places) {
|
||||
minLat = min(minLat, place.latitude);
|
||||
maxLat = max(maxLat, place.latitude);
|
||||
minLong = min(minLong, place.longitude);
|
||||
maxLong = max(maxLong, place.longitude);
|
||||
void _cancelAddPlace() {
|
||||
if (_pendingMarker != null) {
|
||||
setState(() {
|
||||
_markers.remove(_pendingMarker);
|
||||
_pendingMarker = null;
|
||||
});
|
||||
}
|
||||
|
||||
await controller.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(
|
||||
LatLngBounds(
|
||||
southwest: LatLng(minLat, minLong),
|
||||
northeast: LatLng(maxLat, maxLong),
|
||||
),
|
||||
48.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onAddPlacePressed() async {
|
||||
setState(() {
|
||||
final newMarker = Marker(
|
||||
markerId: MarkerId(_lastMapPosition.toString()),
|
||||
position: _lastMapPosition,
|
||||
infoWindow: InfoWindow(title: 'New Place'),
|
||||
draggable: true,
|
||||
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
|
||||
);
|
||||
_markers.add(newMarker);
|
||||
_pendingMarker = newMarker;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _confirmAddPlace(BuildContext context) async {
|
||||
@@ -298,22 +217,21 @@ class PlaceMapState extends State<PlaceMap> {
|
||||
}
|
||||
}
|
||||
|
||||
void _cancelAddPlace() {
|
||||
if (_pendingMarker != null) {
|
||||
setState(() {
|
||||
_markers.remove(_pendingMarker);
|
||||
_pendingMarker = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onToggleMapTypePressed() {
|
||||
final nextType =
|
||||
MapType.values[(_currentMapType.index + 1) % MapType.values.length];
|
||||
|
||||
setState(() {
|
||||
_currentMapType = nextType;
|
||||
});
|
||||
Future<Marker> _createPlaceMarker(BuildContext context, Place place) async {
|
||||
final marker = Marker(
|
||||
markerId: MarkerId(place.latLng.toString()),
|
||||
position: place.latLng,
|
||||
infoWindow: InfoWindow(
|
||||
title: place.name,
|
||||
snippet: '${place.starRating} Star Rating',
|
||||
onTap: () => _pushPlaceDetailsScreen(place),
|
||||
),
|
||||
icon: await _getPlaceMarkerIcon(context, place.category),
|
||||
visible: place.category ==
|
||||
Provider.of<AppState>(context, listen: false).selectedCategory,
|
||||
);
|
||||
_markedPlaces[marker] = place;
|
||||
return marker;
|
||||
}
|
||||
|
||||
Future<void> _maybeUpdateMapConfiguration() async {
|
||||
@@ -351,65 +269,183 @@ class PlaceMapState extends State<PlaceMap> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_maybeUpdateMapConfiguration();
|
||||
var state = Provider.of<AppState>(context);
|
||||
Future<void> _onAddPlacePressed() async {
|
||||
setState(() {
|
||||
final newMarker = Marker(
|
||||
markerId: MarkerId(_lastMapPosition.toString()),
|
||||
position: _lastMapPosition,
|
||||
infoWindow: InfoWindow(title: 'New Place'),
|
||||
draggable: true,
|
||||
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
|
||||
);
|
||||
_markers.add(newMarker);
|
||||
_pendingMarker = newMarker;
|
||||
});
|
||||
}
|
||||
|
||||
return Builder(builder: (context) {
|
||||
// We need this additional builder here so that we can pass its context to
|
||||
// _AddPlaceButtonBar's onSavePressed callback. This callback shows a
|
||||
// SnackBar and to do this, we need a build context that has Scaffold as
|
||||
// an ancestor.
|
||||
return Center(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
GoogleMap(
|
||||
onMapCreated: onMapCreated,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: widget.center,
|
||||
zoom: 11.0,
|
||||
),
|
||||
mapType: _currentMapType,
|
||||
markers: _markers,
|
||||
onCameraMove: (position) => _lastMapPosition = position.target,
|
||||
),
|
||||
_CategoryButtonBar(
|
||||
selectedPlaceCategory: state.selectedCategory,
|
||||
visible: _pendingMarker == null,
|
||||
onChanged: _switchSelectedCategory,
|
||||
),
|
||||
_AddPlaceButtonBar(
|
||||
visible: _pendingMarker != null,
|
||||
onSavePressed: () => _confirmAddPlace(context),
|
||||
onCancelPressed: _cancelAddPlace,
|
||||
),
|
||||
_MapFabs(
|
||||
visible: _pendingMarker == null,
|
||||
onAddPlacePressed: _onAddPlacePressed,
|
||||
onToggleMapTypePressed: _onToggleMapTypePressed,
|
||||
),
|
||||
],
|
||||
void _onPlaceChanged(Place value) {
|
||||
// Replace the place with the modified version.
|
||||
final newPlaces =
|
||||
List<Place>.from(Provider.of<AppState>(context, listen: false).places);
|
||||
final index = newPlaces.indexWhere((place) => place.id == value.id);
|
||||
newPlaces[index] = value;
|
||||
|
||||
_updateExistingPlaceMarker(place: value);
|
||||
|
||||
// Manually update our map configuration here since our map is already
|
||||
// updated with the new marker. Otherwise, the map would be reconfigured
|
||||
// in the main build method due to a modified AppState.
|
||||
_configuration = MapConfiguration(
|
||||
places: newPlaces,
|
||||
selectedCategory:
|
||||
Provider.of<AppState>(context, listen: false).selectedCategory,
|
||||
);
|
||||
|
||||
Provider.of<AppState>(context, listen: false).setPlaces(newPlaces);
|
||||
}
|
||||
|
||||
void _onToggleMapTypePressed() {
|
||||
final nextType =
|
||||
MapType.values[(_currentMapType.index + 1) % MapType.values.length];
|
||||
|
||||
setState(() {
|
||||
_currentMapType = nextType;
|
||||
});
|
||||
}
|
||||
|
||||
void _pushPlaceDetailsScreen(Place place) {
|
||||
assert(place != null);
|
||||
|
||||
Navigator.push<void>(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return PlaceDetails(
|
||||
place: place,
|
||||
onChanged: (value) => _onPlaceChanged(value),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showPlacesForSelectedCategory(PlaceCategory category) async {
|
||||
setState(() {
|
||||
for (var marker in List.of(_markedPlaces.keys)) {
|
||||
final place = _markedPlaces[marker];
|
||||
final updatedMarker = marker.copyWith(
|
||||
visibleParam: place.category == category,
|
||||
);
|
||||
|
||||
_updateMarker(
|
||||
marker: marker,
|
||||
updatedMarker: updatedMarker,
|
||||
place: place,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await _zoomToFitPlaces(_getPlacesForCategory(
|
||||
category,
|
||||
_markedPlaces.values.toList(),
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> _switchSelectedCategory(PlaceCategory category) async {
|
||||
Provider.of<AppState>(context, listen: false).setSelectedCategory(category);
|
||||
await _showPlacesForSelectedCategory(category);
|
||||
}
|
||||
|
||||
void _updateExistingPlaceMarker({@required Place place}) {
|
||||
var marker = _markedPlaces.keys
|
||||
.singleWhere((value) => _markedPlaces[value].id == place.id);
|
||||
|
||||
setState(() {
|
||||
final updatedMarker = marker.copyWith(
|
||||
infoWindowParam: InfoWindow(
|
||||
title: place.name,
|
||||
snippet:
|
||||
place.starRating != 0 ? '${place.starRating} Star Rating' : null,
|
||||
),
|
||||
);
|
||||
_updateMarker(marker: marker, updatedMarker: updatedMarker, place: place);
|
||||
});
|
||||
}
|
||||
|
||||
void _updateMarker({
|
||||
@required Marker marker,
|
||||
@required Marker updatedMarker,
|
||||
@required Place place,
|
||||
}) {
|
||||
_markers.remove(marker);
|
||||
_markedPlaces.remove(marker);
|
||||
|
||||
_markers.add(updatedMarker);
|
||||
_markedPlaces[updatedMarker] = place;
|
||||
}
|
||||
|
||||
Future<void> _zoomToFitPlaces(List<Place> places) async {
|
||||
var controller = await mapController.future;
|
||||
|
||||
// Default min/max values to latitude and longitude of center.
|
||||
var minLat = widget.center.latitude;
|
||||
var maxLat = widget.center.latitude;
|
||||
var minLong = widget.center.longitude;
|
||||
var maxLong = widget.center.longitude;
|
||||
|
||||
for (var place in places) {
|
||||
minLat = min(minLat, place.latitude);
|
||||
maxLat = max(maxLat, place.latitude);
|
||||
minLong = min(minLong, place.longitude);
|
||||
maxLong = max(maxLong, place.longitude);
|
||||
}
|
||||
|
||||
await controller.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(
|
||||
LatLngBounds(
|
||||
southwest: LatLng(minLat, minLong),
|
||||
northeast: LatLng(maxLat, maxLong),
|
||||
),
|
||||
48.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<BitmapDescriptor> _getPlaceMarkerIcon(
|
||||
BuildContext context, PlaceCategory category) async {
|
||||
switch (category) {
|
||||
case PlaceCategory.favorite:
|
||||
return BitmapDescriptor.fromAssetImage(
|
||||
createLocalImageConfiguration(context), 'assets/heart.png');
|
||||
break;
|
||||
case PlaceCategory.visited:
|
||||
return BitmapDescriptor.fromAssetImage(
|
||||
createLocalImageConfiguration(context), 'assets/visited.png');
|
||||
break;
|
||||
case PlaceCategory.wantToGo:
|
||||
default:
|
||||
return BitmapDescriptor.defaultMarker;
|
||||
}
|
||||
}
|
||||
|
||||
static List<Place> _getPlacesForCategory(
|
||||
PlaceCategory category, List<Place> places) {
|
||||
return places.where((place) => place.category == category).toList();
|
||||
}
|
||||
}
|
||||
|
||||
class _CategoryButtonBar extends StatelessWidget {
|
||||
const _CategoryButtonBar({
|
||||
Key key,
|
||||
@required this.selectedPlaceCategory,
|
||||
@required this.visible,
|
||||
@required this.onChanged,
|
||||
}) : assert(selectedPlaceCategory != null),
|
||||
assert(visible != null),
|
||||
assert(onChanged != null),
|
||||
super(key: key);
|
||||
|
||||
final PlaceCategory selectedPlaceCategory;
|
||||
class _AddPlaceButtonBar extends StatelessWidget {
|
||||
final bool visible;
|
||||
final ValueChanged<PlaceCategory> onChanged;
|
||||
|
||||
final VoidCallback onSavePressed;
|
||||
final VoidCallback onCancelPressed;
|
||||
const _AddPlaceButtonBar({
|
||||
Key key,
|
||||
@required this.visible,
|
||||
@required this.onSavePressed,
|
||||
@required this.onCancelPressed,
|
||||
}) : assert(visible != null),
|
||||
assert(onSavePressed != null),
|
||||
assert(onCancelPressed != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -420,7 +456,55 @@ class _CategoryButtonBar extends StatelessWidget {
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ButtonBar(
|
||||
alignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
children: [
|
||||
RaisedButton(
|
||||
color: Colors.blue,
|
||||
child: const Text(
|
||||
'Save',
|
||||
style: TextStyle(color: Colors.white, fontSize: 16.0),
|
||||
),
|
||||
onPressed: onSavePressed,
|
||||
),
|
||||
RaisedButton(
|
||||
color: Colors.red,
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
style: TextStyle(color: Colors.white, fontSize: 16.0),
|
||||
),
|
||||
onPressed: onCancelPressed,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CategoryButtonBar extends StatelessWidget {
|
||||
final PlaceCategory selectedPlaceCategory;
|
||||
final bool visible;
|
||||
final ValueChanged<PlaceCategory> onChanged;
|
||||
|
||||
const _CategoryButtonBar({
|
||||
Key key,
|
||||
@required this.selectedPlaceCategory,
|
||||
@required this.visible,
|
||||
@required this.onChanged,
|
||||
}) : assert(selectedPlaceCategory != null),
|
||||
assert(visible != null),
|
||||
assert(onChanged != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Visibility(
|
||||
visible: visible,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 14.0),
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ButtonBar(
|
||||
alignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
RaisedButton(
|
||||
color: selectedPlaceCategory == PlaceCategory.favorite
|
||||
? Colors.green[700]
|
||||
@@ -458,55 +542,11 @@ class _CategoryButtonBar extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _AddPlaceButtonBar extends StatelessWidget {
|
||||
const _AddPlaceButtonBar({
|
||||
Key key,
|
||||
@required this.visible,
|
||||
@required this.onSavePressed,
|
||||
@required this.onCancelPressed,
|
||||
}) : assert(visible != null),
|
||||
assert(onSavePressed != null),
|
||||
assert(onCancelPressed != null),
|
||||
super(key: key);
|
||||
|
||||
final bool visible;
|
||||
final VoidCallback onSavePressed;
|
||||
final VoidCallback onCancelPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Visibility(
|
||||
visible: visible,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 14.0),
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ButtonBar(
|
||||
alignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
RaisedButton(
|
||||
color: Colors.blue,
|
||||
child: const Text(
|
||||
'Save',
|
||||
style: TextStyle(color: Colors.white, fontSize: 16.0),
|
||||
),
|
||||
onPressed: onSavePressed,
|
||||
),
|
||||
RaisedButton(
|
||||
color: Colors.red,
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
style: TextStyle(color: Colors.white, fontSize: 16.0),
|
||||
),
|
||||
onPressed: onCancelPressed,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MapFabs extends StatelessWidget {
|
||||
final bool visible;
|
||||
final VoidCallback onAddPlacePressed;
|
||||
final VoidCallback onToggleMapTypePressed;
|
||||
|
||||
const _MapFabs({
|
||||
Key key,
|
||||
@required this.visible,
|
||||
@@ -517,10 +557,6 @@ class _MapFabs extends StatelessWidget {
|
||||
assert(onToggleMapTypePressed != null),
|
||||
super(key: key);
|
||||
|
||||
final bool visible;
|
||||
final VoidCallback onAddPlacePressed;
|
||||
final VoidCallback onToggleMapTypePressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@@ -529,7 +565,7 @@ class _MapFabs extends StatelessWidget {
|
||||
child: Visibility(
|
||||
visible: visible,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
children: [
|
||||
FloatingActionButton(
|
||||
heroTag: 'add_place_button',
|
||||
onPressed: onAddPlacePressed,
|
||||
@@ -552,39 +588,3 @@ class _MapFabs extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MapConfiguration {
|
||||
const MapConfiguration({
|
||||
@required this.places,
|
||||
@required this.selectedCategory,
|
||||
}) : assert(places != null),
|
||||
assert(selectedCategory != null);
|
||||
|
||||
final List<Place> places;
|
||||
final PlaceCategory selectedCategory;
|
||||
|
||||
@override
|
||||
int get hashCode => places.hashCode ^ selectedCategory.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return other is MapConfiguration &&
|
||||
other.places == places &&
|
||||
other.selectedCategory == selectedCategory;
|
||||
}
|
||||
|
||||
static MapConfiguration of(AppState appState) {
|
||||
return MapConfiguration(
|
||||
places: appState.places,
|
||||
selectedCategory: appState.selectedCategory,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ class _PlaceTrackerHomePage extends StatelessWidget {
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
children: const [
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0),
|
||||
child: Icon(Icons.pin_drop, size: 24.0),
|
||||
@@ -40,7 +40,7 @@ class _PlaceTrackerHomePage extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.green[700],
|
||||
actions: <Widget>[
|
||||
actions: [
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, 0.0, 16.0, 0.0),
|
||||
child: IconButton(
|
||||
@@ -63,7 +63,7 @@ class _PlaceTrackerHomePage extends StatelessWidget {
|
||||
),
|
||||
body: IndexedStack(
|
||||
index: state.viewType == PlaceTrackerViewType.map ? 0 : 1,
|
||||
children: <Widget>[
|
||||
children: [
|
||||
PlaceMap(center: const LatLng(45.521563, -122.677433)),
|
||||
PlaceList()
|
||||
],
|
||||
@@ -102,7 +102,6 @@ class AppState extends ChangeNotifier {
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is AppState &&
|
||||
other.places == places &&
|
||||
other.selectedCategory == selectedCategory &&
|
||||
|
||||
@@ -141,7 +141,7 @@ class StubData {
|
||||
),
|
||||
];
|
||||
|
||||
static const List<String> reviewStrings = [
|
||||
static const reviewStrings = <String>[
|
||||
'My favorite place in Portland. The employees are wonderful and so is the food. I go here at least once a month!',
|
||||
'Staff was very friendly. Great atmosphere and good music. Would reccommend.',
|
||||
'Best. Place. In. Town. Period.'
|
||||
|
||||
Reference in New Issue
Block a user