import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/location.dart'; import 'package:aves/model/entry/extensions/metadata_edition.dart'; import 'package:aves/model/settings/enums/coordinate_format.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/ref/poi.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/theme/themes.dart'; import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/basic/text_dropdown_button.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/transitions.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/item_picker.dart'; import 'package:aves/widgets/dialogs/pick_dialogs/item_pick_page.dart'; import 'package:aves/widgets/dialogs/pick_dialogs/location_pick_page.dart'; import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; class EditEntryLocationDialog extends StatefulWidget { static const routeName = '/dialog/edit_entry_location'; final AvesEntry entry; final CollectionLens? collection; const EditEntryLocationDialog({ super.key, required this.entry, this.collection, }); @override State createState() => _EditEntryLocationDialogState(); } class _EditEntryLocationDialogState extends State { LocationEditAction _action = LocationEditAction.chooseOnMap; LatLng? _mapCoordinates; late AvesEntry _copyItemSource; final TextEditingController _latitudeController = TextEditingController(), _longitudeController = TextEditingController(); final ValueNotifier _isValidNotifier = ValueNotifier(false); NumberFormat get coordinateFormatter => NumberFormat('0.000000', context.l10n.localeName); @override void initState() { super.initState(); _initMapCoordinates(); _initCopyItem(); _initCustom(); } void _initMapCoordinates() { _mapCoordinates = widget.entry.latLng; } void _initCopyItem() { _copyItemSource = widget.entry; } void _initCustom() { WidgetsBinding.instance.addPostFrameCallback((_) { final latLng = widget.entry.latLng; if (latLng != null) { _latitudeController.text = coordinateFormatter.format(latLng.latitude); _longitudeController.text = coordinateFormatter.format(latLng.longitude); } else { _latitudeController.text = ''; _longitudeController.text = ''; } setState(_validate); }); } @override void dispose() { _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: [ Padding( padding: const EdgeInsets.only(left: 16, top: 8, right: 16), child: TextDropdownButton( values: LocationEditAction.values, valueText: (v) => v.getText(context), value: _action, onChanged: (v) => setState(() { _action = v!; _validate(); }), isExpanded: true, dropdownColor: Themes.thirdLayerColor(context), ), ), AnimatedSwitcher( duration: context.read().formTransition, switchInCurve: Curves.easeInOutCubic, switchOutCurve: Curves.easeInOutCubic, transitionBuilder: AvesTransitions.formTransitionBuilder, child: Column( key: ValueKey(_action), mainAxisSize: MainAxisSize.min, children: [ if (_action == LocationEditAction.chooseOnMap) _buildChooseOnMapContent(context), if (_action == LocationEditAction.copyItem) _buildCopyItemContent(context), if (_action == LocationEditAction.setCustom) _buildSetCustomContent(context), ], ), ), const SizedBox(height: 8), ], actions: [ const CancelButton(), ValueListenableBuilder( valueListenable: _isValidNotifier, builder: (context, isValid, child) { return TextButton( onPressed: isValid ? () => _submit(context) : null, child: Text(l10n.applyButtonLabel), ); }, ), ], ); }), ), ); } Widget _buildChooseOnMapContent(BuildContext context) { final l10n = context.l10n; return Padding( padding: const EdgeInsetsDirectional.only(start: 16, end: 8), child: Row( children: [ Expanded(child: _toText(context, _mapCoordinates)), const SizedBox(width: 8), IconButton( icon: const Icon(AIcons.map), onPressed: _pickLocation, tooltip: l10n.editEntryLocationDialogChooseOnMap, ), ], ), ); } Future _pickLocation() async { final baseCollection = widget.collection; final mapCollection = baseCollection != null ? CollectionLens( source: baseCollection.source, filters: baseCollection.filters, fixedSelection: baseCollection.sortedEntries.where((entry) => entry.hasGps).toList(), ) : null; final latLng = await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: LocationPickPage.routeName), builder: (context) => LocationPickPage( collection: mapCollection, initialLocation: _mapCoordinates, ), fullscreenDialog: true, ), ); mapCollection?.dispose(); if (latLng != null) { settings.mapDefaultCenter = latLng; setState(() { _mapCoordinates = latLng; _validate(); }); } } Widget _buildCopyItemContent(BuildContext context) { return Padding( padding: const EdgeInsetsDirectional.only(start: 16, end: 8), child: Row( children: [ Expanded(child: _toText(context, _copyItemSource.latLng)), const SizedBox(width: 8), ItemPicker( extent: 48, entry: _copyItemSource, onTap: _pickCopyItemSource, ), ], ), ); } Future _pickCopyItemSource() async { final _collection = widget.collection; if (_collection == null) return; final entry = await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: ItemPickPage.routeName), builder: (context) => ItemPickPage( collection: CollectionLens( source: _collection.source, ), ), fullscreenDialog: true, ), ); if (entry != null) { setState(() { _copyItemSource = entry; _validate(); }); } } Widget _buildSetCustomContent(BuildContext context) { final l10n = context.l10n; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( children: [ TextField( controller: _latitudeController, decoration: InputDecoration( labelText: l10n.editEntryLocationDialogLatitude, hintText: coordinateFormatter.format(PointsOfInterest.pointNemo.latitude), ), onChanged: (_) => _validate(), ), TextField( controller: _longitudeController, decoration: InputDecoration( labelText: l10n.editEntryLocationDialogLongitude, hintText: coordinateFormatter.format(PointsOfInterest.pointNemo.longitude), ), onChanged: (_) => _validate(), ), ], ), ), ], ), ); } Text _toText(BuildContext context, LatLng? latLng) { final l10n = context.l10n; if (latLng != null) { return Text( ExtraCoordinateFormat.toDMS(l10n, latLng).join('\n'), ); } else { return Text( l10n.viewerInfoUnknown, style: TextStyle( color: Theme.of(context).textTheme.bodySmall!.color, ), ); } } LatLng? _parseLatLng() { double? tryParse(String text) { try { return double.tryParse(text) ?? (coordinateFormatter.parse(text).toDouble()); } catch (error) { // 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); } void _validate() { switch (_action) { case LocationEditAction.chooseOnMap: _isValidNotifier.value = _mapCoordinates != null; case LocationEditAction.copyItem: _isValidNotifier.value = _copyItemSource.hasGps; case LocationEditAction.setCustom: _isValidNotifier.value = _parseLatLng() != null; case LocationEditAction.remove: _isValidNotifier.value = true; } } void _submit(BuildContext context) { final navigator = Navigator.maybeOf(context); switch (_action) { case LocationEditAction.chooseOnMap: navigator?.pop(_mapCoordinates); case LocationEditAction.copyItem: navigator?.pop(_copyItemSource.latLng); case LocationEditAction.setCustom: navigator?.pop(_parseLatLng()); case LocationEditAction.remove: navigator?.pop(ExtraAvesEntryMetadataEdition.removalLocation); } } }