aves/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart
2022-08-17 20:03:22 +02:00

230 lines
7.8 KiB
Dart

import 'package:aves/model/entry.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves/widgets/dialogs/location_pick_dialog.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:latlong2/latlong.dart';
class EditEntryLocationDialog extends StatefulWidget {
final AvesEntry entry;
final CollectionLens? collection;
const EditEntryLocationDialog({
super.key,
required this.entry,
this.collection,
});
@override
State<EditEntryLocationDialog> createState() => _EditEntryLocationDialogState();
}
class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
_LocationAction _action = _LocationAction.set;
final TextEditingController _latitudeController = TextEditingController(), _longitudeController = TextEditingController();
final FocusNode _latitudeFocusNode = FocusNode(), _longitudeFocusNode = FocusNode();
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
NumberFormat get coordinateFormatter => NumberFormat('0.000000', context.l10n.localeName);
@override
void initState() {
super.initState();
_latitudeFocusNode.addListener(_onLatLngFocusChange);
_longitudeFocusNode.addListener(_onLatLngFocusChange);
WidgetsBinding.instance.addPostFrameCallback((_) => _setLocation(context, widget.entry.latLng));
}
@override
void dispose() {
_latitudeFocusNode.removeListener(_onLatLngFocusChange);
_longitudeFocusNode.removeListener(_onLatLngFocusChange);
_latitudeController.dispose();
_longitudeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MediaQueryDataProvider(
child: TooltipTheme(
data: TooltipTheme.of(context).copyWith(
preferBelow: false,
),
child: Builder(builder: (context) {
final l10n = context.l10n;
return AvesDialog(
title: l10n.editEntryLocationDialogTitle,
scrollableContent: [
RadioListTile<_LocationAction>(
value: _LocationAction.set,
groupValue: _action,
onChanged: (v) => setState(() {
_action = v!;
_validate();
}),
title: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
children: [
TextField(
controller: _latitudeController,
focusNode: _latitudeFocusNode,
decoration: InputDecoration(
labelText: l10n.editEntryLocationDialogLatitude,
hintText: coordinateFormatter.format(Constants.pointNemo.latitude),
),
onChanged: (_) => _validate(),
),
TextField(
controller: _longitudeController,
focusNode: _longitudeFocusNode,
decoration: InputDecoration(
labelText: l10n.editEntryLocationDialogLongitude,
hintText: coordinateFormatter.format(Constants.pointNemo.longitude),
),
onChanged: (_) => _validate(),
),
],
),
),
const SizedBox(width: 8),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: IconButton(
icon: const Icon(AIcons.map),
onPressed: _pickLocation,
tooltip: l10n.editEntryLocationDialogChooseOnMapTooltip,
),
),
],
),
contentPadding: const EdgeInsetsDirectional.only(start: 16, end: 8),
),
RadioListTile<_LocationAction>(
value: _LocationAction.remove,
groupValue: _action,
onChanged: (v) => setState(() {
_action = v!;
_latitudeFocusNode.unfocus();
_longitudeFocusNode.unfocus();
_validate();
}),
title: Text(l10n.actionRemove),
),
],
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
ValueListenableBuilder<bool>(
valueListenable: _isValidNotifier,
builder: (context, isValid, child) {
return TextButton(
onPressed: isValid ? () => _submit(context) : null,
child: Text(l10n.applyButtonLabel),
);
},
),
],
);
}),
),
);
}
void _onLatLngFocusChange() {
if (_latitudeFocusNode.hasFocus || _longitudeFocusNode.hasFocus) {
setState(() {
_action = _LocationAction.set;
_validate();
});
}
}
void _setLocation(BuildContext context, LatLng? latLng) {
_latitudeController.text = latLng != null ? coordinateFormatter.format(latLng.latitude) : '';
_longitudeController.text = latLng != null ? coordinateFormatter.format(latLng.longitude) : '';
setState(() {
_action = _LocationAction.set;
_validate();
});
}
Future<void> _pickLocation() async {
final latLng = await Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: LocationPickDialog.routeName),
builder: (context) {
final baseCollection = widget.collection;
final mapCollection = baseCollection != null
? CollectionLens(
source: baseCollection.source,
filters: baseCollection.filters,
fixedSelection: baseCollection.sortedEntries.where((entry) => entry.hasGps).toList(),
)
: null;
return LocationPickDialog(
collection: mapCollection,
initialLocation: _parseLatLng(),
);
},
fullscreenDialog: true,
),
);
if (latLng != null) {
_setLocation(context, latLng);
}
}
LatLng? _parseLatLng() {
double? tryParse(String text) {
try {
return double.tryParse(text) ?? (coordinateFormatter.parse(text).toDouble());
} catch (e) {
// ignore
return null;
}
}
final lat = tryParse(_latitudeController.text);
final lng = tryParse(_longitudeController.text);
if (lat == null || lng == null) return null;
if (lat < -90 || lat > 90 || lng < -180 || lng > 180) return null;
return LatLng(lat, lng);
}
Future<void> _validate() async {
switch (_action) {
case _LocationAction.set:
_isValidNotifier.value = _parseLatLng() != null;
break;
case _LocationAction.remove:
_isValidNotifier.value = true;
break;
}
}
void _submit(BuildContext context) {
switch (_action) {
case _LocationAction.set:
Navigator.pop(context, _parseLatLng());
break;
case _LocationAction.remove:
Navigator.pop(context, LatLng(0, 0));
break;
}
}
}
enum _LocationAction { set, remove }