#1397 edit location via GPX
This commit is contained in:
parent
a99f4877ce
commit
0301269171
29 changed files with 649 additions and 168 deletions
|
@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## <a id="unreleased"></a>[Unreleased]
|
## <a id="unreleased"></a>[Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- edit location via GPX
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- upgraded Flutter to stable v3.27.3
|
- upgraded Flutter to stable v3.27.3
|
||||||
|
|
|
@ -7,6 +7,7 @@ enum AppMode {
|
||||||
pickFilteredMediaInternal,
|
pickFilteredMediaInternal,
|
||||||
pickUnfilteredMediaInternal,
|
pickUnfilteredMediaInternal,
|
||||||
pickFilterInternal,
|
pickFilterInternal,
|
||||||
|
previewMap,
|
||||||
screenSaver,
|
screenSaver,
|
||||||
setWallpaper,
|
setWallpaper,
|
||||||
slideshow,
|
slideshow,
|
||||||
|
|
|
@ -508,8 +508,10 @@
|
||||||
"editEntryLocationDialogTitle": "Location",
|
"editEntryLocationDialogTitle": "Location",
|
||||||
"editEntryLocationDialogSetCustom": "Set custom location",
|
"editEntryLocationDialogSetCustom": "Set custom location",
|
||||||
"editEntryLocationDialogChooseOnMap": "Choose on map",
|
"editEntryLocationDialogChooseOnMap": "Choose on map",
|
||||||
|
"editEntryLocationDialogImportGpx": "Import GPX",
|
||||||
"editEntryLocationDialogLatitude": "Latitude",
|
"editEntryLocationDialogLatitude": "Latitude",
|
||||||
"editEntryLocationDialogLongitude": "Longitude",
|
"editEntryLocationDialogLongitude": "Longitude",
|
||||||
|
"editEntryLocationDialogTimeShift": "Time shift",
|
||||||
|
|
||||||
"locationPickerUseThisLocationButton": "Use this location",
|
"locationPickerUseThisLocationButton": "Use this location",
|
||||||
|
|
||||||
|
|
|
@ -333,6 +333,11 @@ class Dependencies {
|
||||||
license: mit,
|
license: mit,
|
||||||
sourceUrl: 'https://github.com/fluttercommunity/get_it',
|
sourceUrl: 'https://github.com/fluttercommunity/get_it',
|
||||||
),
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'GPX',
|
||||||
|
license: apache2,
|
||||||
|
sourceUrl: 'https://github.com/kb0/dart-gpx',
|
||||||
|
),
|
||||||
Dependency(
|
Dependency(
|
||||||
name: 'HTTP',
|
name: 'HTTP',
|
||||||
license: bsd3,
|
license: bsd3,
|
||||||
|
|
|
@ -11,11 +11,18 @@ String formatDateTime(DateTime date, String locale, bool use24hour) => [
|
||||||
].join(AText.separator);
|
].join(AText.separator);
|
||||||
|
|
||||||
String formatFriendlyDuration(Duration d) {
|
String formatFriendlyDuration(Duration d) {
|
||||||
final seconds = (d.inSeconds.remainder(Duration.secondsPerMinute)).toString().padLeft(2, '0');
|
final isNegative = d.isNegative;
|
||||||
if (d.inHours == 0) return '${d.inMinutes}:$seconds';
|
final sign = isNegative ? '-' : '';
|
||||||
|
d = d.abs();
|
||||||
|
final hours = d.inHours;
|
||||||
|
d -= Duration(hours: hours);
|
||||||
|
final minutes = d.inMinutes;
|
||||||
|
d -= Duration(minutes: minutes);
|
||||||
|
final seconds = d.inSeconds;
|
||||||
|
|
||||||
final minutes = (d.inMinutes.remainder(Duration.minutesPerHour)).toString().padLeft(2, '0');
|
if (hours == 0) return '$sign$minutes:${seconds.toString().padLeft(2, '0')}';
|
||||||
return '${d.inHours}:$minutes:$seconds';
|
|
||||||
|
return '$sign$hours:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||||
}
|
}
|
||||||
|
|
||||||
String formatPreciseDuration(Duration d) {
|
String formatPreciseDuration(Duration d) {
|
||||||
|
|
|
@ -9,6 +9,7 @@ extension ExtraLocationEditActionView on LocationEditAction {
|
||||||
LocationEditAction.chooseOnMap => l10n.editEntryLocationDialogChooseOnMap,
|
LocationEditAction.chooseOnMap => l10n.editEntryLocationDialogChooseOnMap,
|
||||||
LocationEditAction.copyItem => l10n.editEntryDialogCopyFromItem,
|
LocationEditAction.copyItem => l10n.editEntryDialogCopyFromItem,
|
||||||
LocationEditAction.setCustom => l10n.editEntryLocationDialogSetCustom,
|
LocationEditAction.setCustom => l10n.editEntryLocationDialogSetCustom,
|
||||||
|
LocationEditAction.importGpx => l10n.editEntryLocationDialogImportGpx,
|
||||||
LocationEditAction.remove => l10n.actionRemove,
|
LocationEditAction.remove => l10n.actionRemove,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -563,10 +563,10 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
if (entries == null || entries.isEmpty) return;
|
if (entries == null || entries.isEmpty) return;
|
||||||
|
|
||||||
final collection = context.read<CollectionLens>();
|
final collection = context.read<CollectionLens>();
|
||||||
final location = await selectLocation(context, entries, collection);
|
final locationByEntry = await selectLocation(context, entries, collection);
|
||||||
if (location == null) return;
|
if (locationByEntry == null) return;
|
||||||
|
|
||||||
await _edit(context, entries, (entry) => entry.editLocation(location));
|
await _edit(context, locationByEntry.keys.toSet(), (entry) => entry.editLocation(locationByEntry[entry]));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<LatLng?> editLocationByMap(BuildContext context, Set<AvesEntry> entries, LatLng clusterLocation, CollectionLens mapCollection) async {
|
Future<LatLng?> editLocationByMap(BuildContext context, Set<AvesEntry> entries, LatLng clusterLocation, CollectionLens mapCollection) async {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import 'package:aves/model/entry/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/entry/extensions/metadata_edition.dart';
|
import 'package:aves/model/entry/extensions/metadata_edition.dart';
|
||||||
import 'package:aves/model/entry/extensions/multipage.dart';
|
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||||
|
import 'package:aves/model/filters/covered/tag.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/placeholder.dart';
|
import 'package:aves/model/filters/placeholder.dart';
|
||||||
import 'package:aves/model/filters/covered/tag.dart';
|
|
||||||
import 'package:aves/model/metadata/date_modifier.dart';
|
import 'package:aves/model/metadata/date_modifier.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/ref/mime_types.dart';
|
import 'package:aves/ref/mime_types.dart';
|
||||||
|
@ -17,9 +17,7 @@ import 'package:aves/widgets/dialogs/entry_editors/edit_rating_dialog.dart';
|
||||||
import 'package:aves/widgets/dialogs/entry_editors/remove_metadata_dialog.dart';
|
import 'package:aves/widgets/dialogs/entry_editors/remove_metadata_dialog.dart';
|
||||||
import 'package:aves/widgets/dialogs/entry_editors/tag_editor_page.dart';
|
import 'package:aves/widgets/dialogs/entry_editors/tag_editor_page.dart';
|
||||||
import 'package:aves_model/aves_model.dart';
|
import 'package:aves_model/aves_model.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
|
||||||
|
|
||||||
mixin EntryEditorMixin {
|
mixin EntryEditorMixin {
|
||||||
Future<DateModifier?> selectDateModifier(BuildContext context, Set<AvesEntry> entries, CollectionLens? collection) async {
|
Future<DateModifier?> selectDateModifier(BuildContext context, Set<AvesEntry> entries, CollectionLens? collection) async {
|
||||||
|
@ -35,15 +33,13 @@ mixin EntryEditorMixin {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<LatLng?> selectLocation(BuildContext context, Set<AvesEntry> entries, CollectionLens? collection) async {
|
Future<LocationEditActionResult?> selectLocation(BuildContext context, Set<AvesEntry> entries, CollectionLens? collection) async {
|
||||||
if (entries.isEmpty) return null;
|
if (entries.isEmpty) return null;
|
||||||
|
|
||||||
final entry = entries.firstWhereOrNull((entry) => entry.hasGps) ?? entries.first;
|
return showDialog<LocationEditActionResult>(
|
||||||
|
|
||||||
return showDialog<LatLng>(
|
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => EditEntryLocationDialog(
|
builder: (context) => EditEntryLocationDialog(
|
||||||
entry: entry,
|
entries: entries,
|
||||||
collection: collection,
|
collection: collection,
|
||||||
),
|
),
|
||||||
routeSettings: const RouteSettings(name: EditEntryLocationDialog.routeName),
|
routeSettings: const RouteSettings(name: EditEntryLocationDialog.routeName),
|
||||||
|
|
152
lib/widgets/common/basic/time_shift_selector.dart
Normal file
152
lib/widgets/common/basic/time_shift_selector.dart
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
import 'package:aves/ref/locales.dart';
|
||||||
|
import 'package:aves/utils/time_utils.dart';
|
||||||
|
import 'package:aves/widgets/common/basic/wheel.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
class TimeShiftSelector extends StatefulWidget {
|
||||||
|
final TimeShiftController controller;
|
||||||
|
|
||||||
|
const TimeShiftSelector({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TimeShiftSelector> createState() => _TimeShiftSelectorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimeShiftSelectorState extends State<TimeShiftSelector> {
|
||||||
|
late ValueNotifier<int> _shiftHour, _shiftMinute, _shiftSecond;
|
||||||
|
late ValueNotifier<String> _shiftSign;
|
||||||
|
|
||||||
|
static const _positiveSign = '+';
|
||||||
|
static const _negativeSign = '-';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
var initialValue = widget.controller.initialValue;
|
||||||
|
final sign = initialValue.isNegative ? _negativeSign : _positiveSign;
|
||||||
|
initialValue = initialValue.abs();
|
||||||
|
final hours = initialValue.inHours;
|
||||||
|
initialValue -= Duration(hours: hours);
|
||||||
|
final minutes = initialValue.inMinutes;
|
||||||
|
initialValue -= Duration(minutes: minutes);
|
||||||
|
final seconds = initialValue.inSeconds;
|
||||||
|
|
||||||
|
_shiftSign = ValueNotifier(sign);
|
||||||
|
_shiftHour = ValueNotifier(hours);
|
||||||
|
_shiftMinute = ValueNotifier(minutes);
|
||||||
|
_shiftSecond = ValueNotifier(seconds);
|
||||||
|
|
||||||
|
_shiftSign.addListener(_updateValue);
|
||||||
|
_shiftHour.addListener(_updateValue);
|
||||||
|
_shiftMinute.addListener(_updateValue);
|
||||||
|
_shiftSecond.addListener(_updateValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_shiftSign.dispose();
|
||||||
|
_shiftHour.dispose();
|
||||||
|
_shiftMinute.dispose();
|
||||||
|
_shiftSecond.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final timeComponentFormatter = NumberFormat('0', context.locale);
|
||||||
|
|
||||||
|
const textStyle = TextStyle(fontSize: 34);
|
||||||
|
const digitsAlign = TextAlign.right;
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: Table(
|
||||||
|
textDirection: timeComponentsDirection,
|
||||||
|
children: [
|
||||||
|
TableRow(
|
||||||
|
children: [
|
||||||
|
const SizedBox(),
|
||||||
|
Center(child: Text(l10n.durationDialogHours)),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Center(child: Text(l10n.durationDialogMinutes)),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Center(child: Text(l10n.durationDialogSeconds)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TableRow(
|
||||||
|
children: [
|
||||||
|
WheelSelector(
|
||||||
|
valueNotifier: _shiftSign,
|
||||||
|
values: const [_positiveSign, _negativeSign],
|
||||||
|
textStyle: textStyle,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
format: (v) => v,
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: WheelSelector(
|
||||||
|
valueNotifier: _shiftHour,
|
||||||
|
values: List.generate(hoursInDay, (i) => i),
|
||||||
|
textStyle: textStyle,
|
||||||
|
textAlign: digitsAlign,
|
||||||
|
format: timeComponentFormatter.format,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
':',
|
||||||
|
style: textStyle,
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: WheelSelector(
|
||||||
|
valueNotifier: _shiftMinute,
|
||||||
|
values: List.generate(minutesInHour, (i) => i),
|
||||||
|
textStyle: textStyle,
|
||||||
|
textAlign: digitsAlign,
|
||||||
|
format: timeComponentFormatter.format,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
':',
|
||||||
|
style: textStyle,
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: WheelSelector(
|
||||||
|
valueNotifier: _shiftSecond,
|
||||||
|
values: List.generate(secondsInMinute, (i) => i),
|
||||||
|
textStyle: textStyle,
|
||||||
|
textAlign: digitsAlign,
|
||||||
|
format: timeComponentFormatter.format,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
defaultColumnWidth: const IntrinsicColumnWidth(),
|
||||||
|
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateValue() {
|
||||||
|
final sign = _shiftSign.value == _positiveSign ? 1 : -1;
|
||||||
|
final hours = _shiftHour.value;
|
||||||
|
final minutes = _shiftMinute.value;
|
||||||
|
final seconds = _shiftSecond.value;
|
||||||
|
widget.controller.value = Duration(hours: hours, minutes: minutes, seconds: seconds) * sign;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimeShiftController {
|
||||||
|
final Duration initialValue;
|
||||||
|
Duration value;
|
||||||
|
|
||||||
|
TimeShiftController({required this.initialValue}) : value = initialValue;
|
||||||
|
}
|
|
@ -42,6 +42,7 @@ class GeoMap extends StatefulWidget {
|
||||||
final ValueNotifier<LatLng?>? dotLocationNotifier;
|
final ValueNotifier<LatLng?>? dotLocationNotifier;
|
||||||
final ValueNotifier<double>? overlayOpacityNotifier;
|
final ValueNotifier<double>? overlayOpacityNotifier;
|
||||||
final MapOverlay? overlayEntry;
|
final MapOverlay? overlayEntry;
|
||||||
|
final Set<List<LatLng>>? tracks;
|
||||||
final UserZoomChangeCallback? onUserZoomChange;
|
final UserZoomChangeCallback? onUserZoomChange;
|
||||||
final MapTapCallback? onMapTap;
|
final MapTapCallback? onMapTap;
|
||||||
final void Function(
|
final void Function(
|
||||||
|
@ -69,6 +70,7 @@ class GeoMap extends StatefulWidget {
|
||||||
this.dotLocationNotifier,
|
this.dotLocationNotifier,
|
||||||
this.overlayOpacityNotifier,
|
this.overlayOpacityNotifier,
|
||||||
this.overlayEntry,
|
this.overlayEntry,
|
||||||
|
this.tracks,
|
||||||
this.onUserZoomChange,
|
this.onUserZoomChange,
|
||||||
this.onMapTap,
|
this.onMapTap,
|
||||||
this.onMarkerTap,
|
this.onMarkerTap,
|
||||||
|
@ -179,6 +181,7 @@ class _GeoMapState extends State<GeoMap> {
|
||||||
dotLocationNotifier: widget.dotLocationNotifier,
|
dotLocationNotifier: widget.dotLocationNotifier,
|
||||||
overlayOpacityNotifier: widget.overlayOpacityNotifier,
|
overlayOpacityNotifier: widget.overlayOpacityNotifier,
|
||||||
overlayEntry: widget.overlayEntry,
|
overlayEntry: widget.overlayEntry,
|
||||||
|
tracks: widget.tracks,
|
||||||
onUserZoomChange: widget.onUserZoomChange,
|
onUserZoomChange: widget.onUserZoomChange,
|
||||||
onMapTap: widget.onMapTap,
|
onMapTap: widget.onMapTap,
|
||||||
onMarkerTap: _onMarkerTap,
|
onMarkerTap: _onMarkerTap,
|
||||||
|
@ -210,6 +213,7 @@ class _GeoMapState extends State<GeoMap> {
|
||||||
),
|
),
|
||||||
overlayOpacityNotifier: widget.overlayOpacityNotifier,
|
overlayOpacityNotifier: widget.overlayOpacityNotifier,
|
||||||
overlayEntry: widget.overlayEntry,
|
overlayEntry: widget.overlayEntry,
|
||||||
|
tracks: widget.tracks,
|
||||||
onUserZoomChange: widget.onUserZoomChange,
|
onUserZoomChange: widget.onUserZoomChange,
|
||||||
onMapTap: widget.onMapTap,
|
onMapTap: widget.onMapTap,
|
||||||
onMarkerTap: _onMarkerTap,
|
onMarkerTap: _onMarkerTap,
|
||||||
|
|
|
@ -30,6 +30,7 @@ class EntryLeafletMap<T> extends StatefulWidget {
|
||||||
final Size markerSize, dotMarkerSize;
|
final Size markerSize, dotMarkerSize;
|
||||||
final ValueNotifier<double>? overlayOpacityNotifier;
|
final ValueNotifier<double>? overlayOpacityNotifier;
|
||||||
final MapOverlay? overlayEntry;
|
final MapOverlay? overlayEntry;
|
||||||
|
final Set<List<LatLng>>? tracks;
|
||||||
final UserZoomChangeCallback? onUserZoomChange;
|
final UserZoomChangeCallback? onUserZoomChange;
|
||||||
final MapTapCallback? onMapTap;
|
final MapTapCallback? onMapTap;
|
||||||
final MarkerTapCallback<T>? onMarkerTap;
|
final MarkerTapCallback<T>? onMarkerTap;
|
||||||
|
@ -52,6 +53,7 @@ class EntryLeafletMap<T> extends StatefulWidget {
|
||||||
required this.dotMarkerSize,
|
required this.dotMarkerSize,
|
||||||
this.overlayOpacityNotifier,
|
this.overlayOpacityNotifier,
|
||||||
this.overlayEntry,
|
this.overlayEntry,
|
||||||
|
this.tracks,
|
||||||
this.onUserZoomChange,
|
this.onUserZoomChange,
|
||||||
this.onMapTap,
|
this.onMapTap,
|
||||||
this.onMarkerTap,
|
this.onMarkerTap,
|
||||||
|
@ -175,6 +177,7 @@ class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProv
|
||||||
children: [
|
children: [
|
||||||
_buildMapLayer(),
|
_buildMapLayer(),
|
||||||
if (widget.overlayEntry != null) _buildOverlayImageLayer(),
|
if (widget.overlayEntry != null) _buildOverlayImageLayer(),
|
||||||
|
if (widget.tracks != null) _buildTracksLayer(),
|
||||||
MarkerLayer(
|
MarkerLayer(
|
||||||
markers: markers,
|
markers: markers,
|
||||||
rotate: true,
|
rotate: true,
|
||||||
|
@ -243,6 +246,22 @@ class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProv
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildTracksLayer() {
|
||||||
|
final tracks = widget.tracks;
|
||||||
|
if (tracks == null) return const SizedBox();
|
||||||
|
|
||||||
|
final trackColor = Theme.of(context).colorScheme.primary;
|
||||||
|
return PolylineLayer(
|
||||||
|
polylines: tracks
|
||||||
|
.map((v) => Polyline(
|
||||||
|
points: v,
|
||||||
|
strokeWidth: MapThemeData.trackWidth.toDouble(),
|
||||||
|
color: trackColor,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _onBoundsChanged() => _debouncer(_onIdle);
|
void _onBoundsChanged() => _debouncer(_onIdle);
|
||||||
|
|
||||||
void _onIdle() {
|
void _onIdle() {
|
||||||
|
|
|
@ -1,24 +1,21 @@
|
||||||
import 'package:aves/model/entry/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/metadata/date_modifier.dart';
|
import 'package:aves/model/metadata/date_modifier.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/ref/locales.dart';
|
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/format.dart';
|
import 'package:aves/theme/format.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/theme/themes.dart';
|
import 'package:aves/theme/themes.dart';
|
||||||
import 'package:aves/utils/time_utils.dart';
|
|
||||||
import 'package:aves/view/view.dart';
|
import 'package:aves/view/view.dart';
|
||||||
import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
|
import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
|
||||||
import 'package:aves/widgets/common/basic/wheel.dart';
|
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/fx/transitions.dart';
|
import 'package:aves/widgets/common/fx/transitions.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.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/aves_dialog.dart';
|
||||||
|
import 'package:aves/widgets/common/basic/time_shift_selector.dart';
|
||||||
import 'package:aves/widgets/dialogs/item_picker.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/item_pick_page.dart';
|
||||||
import 'package:aves_model/aves_model.dart';
|
import 'package:aves_model/aves_model.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class EditEntryDateDialog extends StatefulWidget {
|
class EditEntryDateDialog extends StatefulWidget {
|
||||||
|
@ -42,17 +39,13 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
DateFieldSource _copyFieldSource = DateFieldSource.fileModifiedDate;
|
DateFieldSource _copyFieldSource = DateFieldSource.fileModifiedDate;
|
||||||
late AvesEntry _copyItemSource;
|
late AvesEntry _copyItemSource;
|
||||||
late DateTime _customDateTime;
|
late DateTime _customDateTime;
|
||||||
late ValueNotifier<int> _shiftHour, _shiftMinute, _shiftSecond;
|
late TimeShiftController _timeShiftController;
|
||||||
late ValueNotifier<String> _shiftSign;
|
|
||||||
bool _showOptions = false;
|
bool _showOptions = false;
|
||||||
final Set<MetadataField> _fields = {...DateModifier.writableFields};
|
final Set<MetadataField> _fields = {...DateModifier.writableFields};
|
||||||
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
||||||
|
|
||||||
DateTime get copyItemDate => _copyItemSource.bestDate ?? DateTime.now();
|
DateTime get copyItemDate => _copyItemSource.bestDate ?? DateTime.now();
|
||||||
|
|
||||||
static const _positiveSign = '+';
|
|
||||||
static const _negativeSign = '-';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
@ -65,10 +58,6 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_isValidNotifier.dispose();
|
_isValidNotifier.dispose();
|
||||||
_shiftHour.dispose();
|
|
||||||
_shiftMinute.dispose();
|
|
||||||
_shiftSecond.dispose();
|
|
||||||
_shiftSign.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,10 +70,9 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initShift() {
|
void _initShift() {
|
||||||
_shiftHour = ValueNotifier(1);
|
_timeShiftController = TimeShiftController(
|
||||||
_shiftMinute = ValueNotifier(0);
|
initialValue: const Duration(hours: 1),
|
||||||
_shiftSecond = ValueNotifier(0);
|
);
|
||||||
_shiftSign = ValueNotifier(_positiveSign);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -203,80 +191,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildShiftContent(BuildContext context) {
|
Widget _buildShiftContent(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
return TimeShiftSelector(controller: _timeShiftController);
|
||||||
final timeComponentFormatter = NumberFormat('0', context.locale);
|
|
||||||
|
|
||||||
const textStyle = TextStyle(fontSize: 34);
|
|
||||||
const digitsAlign = TextAlign.right;
|
|
||||||
|
|
||||||
return Center(
|
|
||||||
child: Table(
|
|
||||||
textDirection: timeComponentsDirection,
|
|
||||||
children: [
|
|
||||||
TableRow(
|
|
||||||
children: [
|
|
||||||
const SizedBox(),
|
|
||||||
Center(child: Text(l10n.durationDialogHours)),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Center(child: Text(l10n.durationDialogMinutes)),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Center(child: Text(l10n.durationDialogSeconds)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
TableRow(
|
|
||||||
children: [
|
|
||||||
WheelSelector(
|
|
||||||
valueNotifier: _shiftSign,
|
|
||||||
values: const [_positiveSign, _negativeSign],
|
|
||||||
textStyle: textStyle,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
format: (v) => v,
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: WheelSelector(
|
|
||||||
valueNotifier: _shiftHour,
|
|
||||||
values: List.generate(hoursInDay, (i) => i),
|
|
||||||
textStyle: textStyle,
|
|
||||||
textAlign: digitsAlign,
|
|
||||||
format: timeComponentFormatter.format,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Text(
|
|
||||||
':',
|
|
||||||
style: textStyle,
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: WheelSelector(
|
|
||||||
valueNotifier: _shiftMinute,
|
|
||||||
values: List.generate(minutesInHour, (i) => i),
|
|
||||||
textStyle: textStyle,
|
|
||||||
textAlign: digitsAlign,
|
|
||||||
format: timeComponentFormatter.format,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Text(
|
|
||||||
':',
|
|
||||||
style: textStyle,
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: WheelSelector(
|
|
||||||
valueNotifier: _shiftSecond,
|
|
||||||
values: List.generate(secondsInMinute, (i) => i),
|
|
||||||
textStyle: textStyle,
|
|
||||||
textAlign: digitsAlign,
|
|
||||||
format: timeComponentFormatter.format,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
defaultColumnWidth: const IntrinsicColumnWidth(),
|
|
||||||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDestinationFields(BuildContext context) {
|
Widget _buildDestinationFields(BuildContext context) {
|
||||||
|
@ -368,7 +283,6 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
fullscreenDialog: true,
|
fullscreenDialog: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
pickCollection.dispose();
|
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
setState(() => _copyItemSource = entry);
|
setState(() => _copyItemSource = entry);
|
||||||
}
|
}
|
||||||
|
@ -388,8 +302,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
case DateEditAction.extractFromTitle:
|
case DateEditAction.extractFromTitle:
|
||||||
return DateModifier.extractFromTitle();
|
return DateModifier.extractFromTitle();
|
||||||
case DateEditAction.shift:
|
case DateEditAction.shift:
|
||||||
final shiftTotalSeconds = ((_shiftHour.value * minutesInHour + _shiftMinute.value) * secondsInMinute + _shiftSecond.value) * (_shiftSign.value == _positiveSign ? 1 : -1);
|
return DateModifier.shift(_fields, _timeShiftController.value.inSeconds);
|
||||||
return DateModifier.shift(_fields, shiftTotalSeconds);
|
|
||||||
case DateEditAction.remove:
|
case DateEditAction.remove:
|
||||||
return DateModifier.remove(_fields);
|
return DateModifier.remove(_fields);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,40 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/entry/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/entry/extensions/location.dart';
|
import 'package:aves/model/entry/extensions/location.dart';
|
||||||
import 'package:aves/model/entry/extensions/metadata_edition.dart';
|
import 'package:aves/model/entry/extensions/metadata_edition.dart';
|
||||||
|
import 'package:aves/model/entry/sort.dart';
|
||||||
import 'package:aves/model/filters/covered/location.dart';
|
import 'package:aves/model/filters/covered/location.dart';
|
||||||
|
import 'package:aves/model/metadata/catalog.dart';
|
||||||
import 'package:aves/model/settings/enums/coordinate_format.dart';
|
import 'package:aves/model/settings/enums/coordinate_format.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/ref/poi.dart';
|
import 'package:aves/ref/poi.dart';
|
||||||
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/theme/format.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/theme/themes.dart';
|
import 'package:aves/theme/themes.dart';
|
||||||
import 'package:aves/view/view.dart';
|
import 'package:aves/view/view.dart';
|
||||||
import 'package:aves/widgets/aves_app.dart';
|
import 'package:aves/widgets/aves_app.dart';
|
||||||
|
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||||
import 'package:aves/widgets/common/basic/text_dropdown_button.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/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/fx/transitions.dart';
|
import 'package:aves/widgets/common/fx/transitions.dart';
|
||||||
|
import 'package:aves/widgets/common/identity/aves_caption.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.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/aves_dialog.dart';
|
||||||
import 'package:aves/widgets/dialogs/item_picker.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/item_pick_page.dart';
|
||||||
import 'package:aves/widgets/dialogs/pick_dialogs/location_pick_page.dart';
|
import 'package:aves/widgets/dialogs/pick_dialogs/location_pick_page.dart';
|
||||||
|
import 'package:aves/widgets/dialogs/time_shift_dialog.dart';
|
||||||
|
import 'package:aves/widgets/map/map_page.dart';
|
||||||
import 'package:aves_model/aves_model.dart';
|
import 'package:aves_model/aves_model.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gpx/gpx.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -31,12 +42,12 @@ import 'package:provider/provider.dart';
|
||||||
class EditEntryLocationDialog extends StatefulWidget {
|
class EditEntryLocationDialog extends StatefulWidget {
|
||||||
static const routeName = '/dialog/edit_entry_location';
|
static const routeName = '/dialog/edit_entry_location';
|
||||||
|
|
||||||
final AvesEntry entry;
|
final Set<AvesEntry> entries;
|
||||||
final CollectionLens? collection;
|
final CollectionLens? collection;
|
||||||
|
|
||||||
const EditEntryLocationDialog({
|
const EditEntryLocationDialog({
|
||||||
super.key,
|
super.key,
|
||||||
required this.entry,
|
required this.entries,
|
||||||
this.collection,
|
this.collection,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -44,19 +55,26 @@ class EditEntryLocationDialog extends StatefulWidget {
|
||||||
State<EditEntryLocationDialog> createState() => _EditEntryLocationDialogState();
|
State<EditEntryLocationDialog> createState() => _EditEntryLocationDialogState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> with FeedbackMixin {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
LocationEditAction _action = LocationEditAction.chooseOnMap;
|
LocationEditAction _action = LocationEditAction.chooseOnMap;
|
||||||
LatLng? _mapCoordinates;
|
LatLng? _mapCoordinates;
|
||||||
|
late final AvesEntry mainEntry;
|
||||||
late AvesEntry _copyItemSource;
|
late AvesEntry _copyItemSource;
|
||||||
|
Gpx? _gpx;
|
||||||
|
Duration _gpxShift = Duration.zero;
|
||||||
|
final Map<AvesEntry, LatLng> _gpxMap = {};
|
||||||
final TextEditingController _latitudeController = TextEditingController(), _longitudeController = TextEditingController();
|
final TextEditingController _latitudeController = TextEditingController(), _longitudeController = TextEditingController();
|
||||||
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
||||||
|
|
||||||
NumberFormat get coordinateFormatter => NumberFormat('0.000000', context.locale);
|
NumberFormat get coordinateFormatter => NumberFormat('0.000000', context.locale);
|
||||||
|
static const _minTimeToGpxPoint = Duration(hours: 1);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
final entries = widget.entries;
|
||||||
|
mainEntry = entries.firstWhereOrNull((entry) => entry.hasGps) ?? entries.first;
|
||||||
_initMapCoordinates();
|
_initMapCoordinates();
|
||||||
_initCopyItem();
|
_initCopyItem();
|
||||||
_initCustom();
|
_initCustom();
|
||||||
|
@ -64,16 +82,16 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initMapCoordinates() {
|
void _initMapCoordinates() {
|
||||||
_mapCoordinates = widget.entry.latLng;
|
_mapCoordinates = mainEntry.latLng;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initCopyItem() {
|
void _initCopyItem() {
|
||||||
_copyItemSource = widget.entry;
|
_copyItemSource = mainEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initCustom() {
|
void _initCustom() {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final latLng = widget.entry.latLng;
|
final latLng = mainEntry.latLng;
|
||||||
if (latLng != null) {
|
if (latLng != null) {
|
||||||
_latitudeController.text = coordinateFormatter.format(latLng.latitude);
|
_latitudeController.text = coordinateFormatter.format(latLng.latitude);
|
||||||
_longitudeController.text = coordinateFormatter.format(latLng.longitude);
|
_longitudeController.text = coordinateFormatter.format(latLng.longitude);
|
||||||
|
@ -128,14 +146,9 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
switchInCurve: Curves.easeInOutCubic,
|
switchInCurve: Curves.easeInOutCubic,
|
||||||
switchOutCurve: Curves.easeInOutCubic,
|
switchOutCurve: Curves.easeInOutCubic,
|
||||||
transitionBuilder: AvesTransitions.formTransitionBuilder,
|
transitionBuilder: AvesTransitions.formTransitionBuilder,
|
||||||
child: Column(
|
child: KeyedSubtree(
|
||||||
key: ValueKey(_action),
|
key: ValueKey(_action),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: _buildContent(),
|
||||||
children: [
|
|
||||||
if (_action == LocationEditAction.chooseOnMap) _buildChooseOnMapContent(context),
|
|
||||||
if (_action == LocationEditAction.copyItem) _buildCopyItemContent(context),
|
|
||||||
if (_action == LocationEditAction.setCustom) _buildSetCustomContent(context),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
@ -158,12 +171,27 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildContent() {
|
||||||
|
switch (_action) {
|
||||||
|
case LocationEditAction.chooseOnMap:
|
||||||
|
return _buildChooseOnMapContent(context);
|
||||||
|
case LocationEditAction.copyItem:
|
||||||
|
return _buildCopyItemContent(context);
|
||||||
|
case LocationEditAction.setCustom:
|
||||||
|
return _buildSetCustomContent(context);
|
||||||
|
case LocationEditAction.importGpx:
|
||||||
|
return _buildImportGpxContent(context);
|
||||||
|
case LocationEditAction.remove:
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildChooseOnMapContent(BuildContext context) {
|
Widget _buildChooseOnMapContent(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
|
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: _toText(context, _mapCoordinates)),
|
Expanded(child: _coordinatesText(context, _mapCoordinates)),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(AIcons.map),
|
icon: const Icon(AIcons.map),
|
||||||
|
@ -179,8 +207,7 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
_latitudeController.text = coordinateFormatter.format(latLng.latitude);
|
_latitudeController.text = coordinateFormatter.format(latLng.latitude);
|
||||||
_longitudeController.text = coordinateFormatter.format(latLng.longitude);
|
_longitudeController.text = coordinateFormatter.format(latLng.longitude);
|
||||||
_action = LocationEditAction.setCustom;
|
_action = LocationEditAction.setCustom;
|
||||||
_validate();
|
setState(_validate);
|
||||||
setState(() {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CollectionLens? _createPickCollection() {
|
CollectionLens? _createPickCollection() {
|
||||||
|
@ -208,7 +235,6 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
fullscreenDialog: true,
|
fullscreenDialog: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
pickCollection?.dispose();
|
|
||||||
if (latLng != null) {
|
if (latLng != null) {
|
||||||
settings.mapDefaultCenter = latLng;
|
settings.mapDefaultCenter = latLng;
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@ -223,7 +249,7 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
|
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: _toText(context, _copyItemSource.latLng)),
|
Expanded(child: _coordinatesText(context, _copyItemSource.latLng)),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
ItemPicker(
|
ItemPicker(
|
||||||
extent: 48,
|
extent: 48,
|
||||||
|
@ -249,7 +275,6 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
fullscreenDialog: true,
|
fullscreenDialog: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
pickCollection.dispose();
|
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_copyItemSource = entry;
|
_copyItemSource = entry;
|
||||||
|
@ -293,13 +318,207 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Text _toText(BuildContext context, LatLng? latLng) {
|
Widget _buildImportGpxContent(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
if (latLng != null) {
|
return Padding(
|
||||||
return Text(
|
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
|
||||||
ExtraCoordinateFormat.toDMS(l10n, latLng).join('\n'),
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _gpxDateRangeText(context, _gpx)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(AIcons.fileImport),
|
||||||
|
onPressed: _pickGpx,
|
||||||
|
tooltip: l10n.pickTooltip,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_gpx != null) ...[
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(l10n.editEntryLocationDialogTimeShift),
|
||||||
|
AvesCaption(_formatShiftDuration(_gpxShift)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(AIcons.edit),
|
||||||
|
onPressed: _pickGpxShift,
|
||||||
|
tooltip: l10n.changeTooltip,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(l10n.statsWithGps(_gpxMap.length))),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(AIcons.map),
|
||||||
|
onPressed: _previewGpx,
|
||||||
|
tooltip: l10n.openMapPageTooltip,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickGpx() async {
|
||||||
|
final bytes = await storageService.openFile();
|
||||||
|
if (bytes.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
final allXmlString = utf8.decode(bytes);
|
||||||
|
final gpx = GpxReader().fromString(allXmlString);
|
||||||
|
|
||||||
|
_gpx = gpx;
|
||||||
|
_gpxShift = Duration.zero;
|
||||||
|
_updateGpxMapping();
|
||||||
|
|
||||||
|
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
|
||||||
|
} catch (error, stack) {
|
||||||
|
debugPrint('failed to import GPX, error=$error\n$stack');
|
||||||
|
showFeedback(context, FeedbackType.warn, context.l10n.genericFailureFeedback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickGpxShift() async {
|
||||||
|
final newShift = await showDialog<Duration>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => TimeShiftDialog(
|
||||||
|
initialValue: _gpxShift,
|
||||||
|
),
|
||||||
|
routeSettings: const RouteSettings(name: TimeShiftDialog.routeName),
|
||||||
|
);
|
||||||
|
if (newShift == null) return;
|
||||||
|
|
||||||
|
_gpxShift = newShift;
|
||||||
|
_updateGpxMapping();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatShiftDuration(Duration duration) {
|
||||||
|
final sign = duration.isNegative ? '-' : '+';
|
||||||
|
duration = duration.abs();
|
||||||
|
final hours = duration.inHours;
|
||||||
|
duration -= Duration(hours: hours);
|
||||||
|
final minutes = duration.inMinutes;
|
||||||
|
duration -= Duration(minutes: minutes);
|
||||||
|
final seconds = duration.inSeconds;
|
||||||
|
return '$sign$hours:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateGpxMapping() {
|
||||||
|
_gpxMap.clear();
|
||||||
|
|
||||||
|
final gpx = _gpx;
|
||||||
|
if (gpx == null) return;
|
||||||
|
|
||||||
|
final Map<AvesEntry, Wpt> wptByEntry = {};
|
||||||
|
|
||||||
|
// dated items and points, oldest first
|
||||||
|
final sortedEntries = widget.entries.where((v) => v.bestDate != null).sorted(AvesEntrySort.compareByDate).reversed.toList();
|
||||||
|
final sortedPoints = gpx.trks.expand((trk) => trk.trksegs).expand((trkSeg) => trkSeg.trkpts).where((v) => v.time != null).sortedBy((v) => v.time!);
|
||||||
|
if (sortedEntries.isNotEmpty && sortedPoints.isNotEmpty) {
|
||||||
|
int entryIndex = 0;
|
||||||
|
int pointIndex = 0;
|
||||||
|
final int maxDurationSecs = const Duration(days: 365).inSeconds;
|
||||||
|
int smallestDifferenceSecs = maxDurationSecs;
|
||||||
|
while (entryIndex < sortedEntries.length && pointIndex < sortedPoints.length) {
|
||||||
|
final entry = sortedEntries[entryIndex];
|
||||||
|
final point = sortedPoints[pointIndex];
|
||||||
|
final entryDate = entry.bestDate!;
|
||||||
|
final pointTime = point.time!.add(_gpxShift);
|
||||||
|
final differenceSecs = entryDate.difference(pointTime).inSeconds.abs();
|
||||||
|
if (differenceSecs < smallestDifferenceSecs) {
|
||||||
|
smallestDifferenceSecs = differenceSecs;
|
||||||
|
wptByEntry[entry] = point;
|
||||||
|
pointIndex++;
|
||||||
} else {
|
} else {
|
||||||
|
smallestDifferenceSecs = maxDurationSecs;
|
||||||
|
entryIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_gpxMap.addEntries(wptByEntry.entries.map((kv) {
|
||||||
|
final entry = kv.key;
|
||||||
|
final wpt = kv.value;
|
||||||
|
final timeToPoint = entry.bestDate!.difference(wpt.time!.add(_gpxShift)).abs();
|
||||||
|
if (timeToPoint < _minTimeToGpxPoint) {
|
||||||
|
final lat = wpt.lat;
|
||||||
|
final lon = wpt.lon;
|
||||||
|
if (lat != null && lon != null) {
|
||||||
|
return MapEntry(entry, LatLng(lat, lon));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).nonNulls);
|
||||||
|
|
||||||
|
setState(_validate);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _previewGpx() async {
|
||||||
|
final source = widget.collection?.source;
|
||||||
|
if (source == null) return;
|
||||||
|
|
||||||
|
final previewEntries = _gpxMap.entries.map((kv) {
|
||||||
|
final entry = kv.key.copyWith();
|
||||||
|
final latLng = kv.value;
|
||||||
|
final catalogMetadata = entry.catalogMetadata?.copyWith() ?? CatalogMetadata(id: entry.id);
|
||||||
|
catalogMetadata.latitude = latLng.latitude;
|
||||||
|
catalogMetadata.longitude = latLng.longitude;
|
||||||
|
entry.catalogMetadata = catalogMetadata;
|
||||||
|
return entry;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final mapCollection = CollectionLens(
|
||||||
|
source: source,
|
||||||
|
listenToSource: false,
|
||||||
|
fixedSelection: previewEntries,
|
||||||
|
);
|
||||||
|
|
||||||
|
final tracks = _gpx?.trks
|
||||||
|
.expand((trk) => trk.trksegs)
|
||||||
|
.map((trkSeg) => trkSeg.trkpts
|
||||||
|
.map((wpt) {
|
||||||
|
final lat = wpt.lat;
|
||||||
|
final lon = wpt.lon;
|
||||||
|
return (lat != null && lon != null) ? LatLng(lat, lon) : null;
|
||||||
|
})
|
||||||
|
.nonNulls
|
||||||
|
.toList())
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
await Navigator.maybeOf(context)?.push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
settings: const RouteSettings(name: LocationPickPage.routeName),
|
||||||
|
builder: (context) {
|
||||||
|
return ListenableProvider<ValueNotifier<AppMode>>.value(
|
||||||
|
value: ValueNotifier(AppMode.previewMap),
|
||||||
|
child: MapPage(
|
||||||
|
collection: mapCollection,
|
||||||
|
tracks: tracks,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
fullscreenDialog: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Text _unknownText(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
return Text(
|
return Text(
|
||||||
l10n.viewerInfoUnknown,
|
l10n.viewerInfoUnknown,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
@ -307,6 +526,39 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(DateTime, DateTime)? _gpxDateRange(Gpx? gpx) {
|
||||||
|
final firstDate = gpx?.trks.firstOrNull?.trksegs.firstOrNull?.trkpts.firstOrNull?.time;
|
||||||
|
final lastDate = gpx?.trks.lastOrNull?.trksegs.lastOrNull?.trkpts.lastOrNull?.time;
|
||||||
|
return firstDate != null && lastDate != null ? (firstDate, lastDate) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text _gpxDateRangeText(BuildContext context, Gpx? gpx) {
|
||||||
|
final dateRange = _gpxDateRange(gpx);
|
||||||
|
if (dateRange != null) {
|
||||||
|
final (firstDate, lastDate) = dateRange;
|
||||||
|
final locale = context.locale;
|
||||||
|
final use24hour = MediaQuery.alwaysUse24HourFormatOf(context);
|
||||||
|
return Text(
|
||||||
|
[
|
||||||
|
formatDateTime(firstDate.toLocal(), locale, use24hour),
|
||||||
|
formatDateTime(lastDate.toLocal(), locale, use24hour),
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return _unknownText(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text _coordinatesText(BuildContext context, LatLng? latLng) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
if (latLng != null) {
|
||||||
|
return Text(
|
||||||
|
ExtraCoordinateFormat.toDMS(l10n, latLng).join('\n'),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return _unknownText(context);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LatLng? _parseLatLng() {
|
LatLng? _parseLatLng() {
|
||||||
|
@ -334,6 +586,8 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
_isValidNotifier.value = _copyItemSource.hasGps;
|
_isValidNotifier.value = _copyItemSource.hasGps;
|
||||||
case LocationEditAction.setCustom:
|
case LocationEditAction.setCustom:
|
||||||
_isValidNotifier.value = _parseLatLng() != null;
|
_isValidNotifier.value = _parseLatLng() != null;
|
||||||
|
case LocationEditAction.importGpx:
|
||||||
|
_isValidNotifier.value = _gpxMap.isNotEmpty;
|
||||||
case LocationEditAction.remove:
|
case LocationEditAction.remove:
|
||||||
_isValidNotifier.value = true;
|
_isValidNotifier.value = true;
|
||||||
}
|
}
|
||||||
|
@ -341,15 +595,23 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
|
|
||||||
void _submit(BuildContext context) {
|
void _submit(BuildContext context) {
|
||||||
final navigator = Navigator.maybeOf(context);
|
final navigator = Navigator.maybeOf(context);
|
||||||
|
final entries = widget.entries;
|
||||||
|
final LocationEditActionResult result = {};
|
||||||
|
void addLocationForAllEntries(LatLng? latLng) => result.addEntries(entries.map((v) => MapEntry(v, latLng)));
|
||||||
switch (_action) {
|
switch (_action) {
|
||||||
case LocationEditAction.chooseOnMap:
|
case LocationEditAction.chooseOnMap:
|
||||||
navigator?.pop(_mapCoordinates);
|
addLocationForAllEntries(_mapCoordinates);
|
||||||
case LocationEditAction.copyItem:
|
case LocationEditAction.copyItem:
|
||||||
navigator?.pop(_copyItemSource.latLng);
|
addLocationForAllEntries(_copyItemSource.latLng);
|
||||||
case LocationEditAction.setCustom:
|
case LocationEditAction.setCustom:
|
||||||
navigator?.pop(_parseLatLng());
|
addLocationForAllEntries(_parseLatLng());
|
||||||
|
case LocationEditAction.importGpx:
|
||||||
|
result.addAll(_gpxMap);
|
||||||
case LocationEditAction.remove:
|
case LocationEditAction.remove:
|
||||||
navigator?.pop(ExtraAvesEntryMetadataEdition.removalLocation);
|
addLocationForAllEntries(ExtraAvesEntryMetadataEdition.removalLocation);
|
||||||
}
|
}
|
||||||
|
navigator?.pop(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef LocationEditActionResult = Map<AvesEntry, LatLng?>;
|
||||||
|
|
|
@ -33,17 +33,18 @@ class ItemPickPage extends StatefulWidget {
|
||||||
class _ItemPickPageState extends State<ItemPickPage> {
|
class _ItemPickPageState extends State<ItemPickPage> {
|
||||||
final ValueNotifier<AppMode> _appModeNotifier = ValueNotifier(AppMode.initialization);
|
final ValueNotifier<AppMode> _appModeNotifier = ValueNotifier(AppMode.initialization);
|
||||||
|
|
||||||
CollectionLens get collection => widget.collection;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
collection.dispose();
|
|
||||||
_appModeNotifier.dispose();
|
_appModeNotifier.dispose();
|
||||||
|
// provided collection should be a new instance specifically created
|
||||||
|
// for the `ItemPickPage` widget, so it can be safely disposed here
|
||||||
|
widget.collection.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final collection = widget.collection;
|
||||||
final liveFilter = collection.filters.firstWhereOrNull((v) => v is QueryFilter && v.live) as QueryFilter?;
|
final liveFilter = collection.filters.firstWhereOrNull((v) => v is QueryFilter && v.live) as QueryFilter?;
|
||||||
_appModeNotifier.value = widget.canRemoveFilters ? AppMode.pickUnfilteredMediaInternal : AppMode.pickFilteredMediaInternal;
|
_appModeNotifier.value = widget.canRemoveFilters ? AppMode.pickUnfilteredMediaInternal : AppMode.pickFilteredMediaInternal;
|
||||||
return ListenableProvider<ValueNotifier<AppMode>>.value(
|
return ListenableProvider<ValueNotifier<AppMode>>.value(
|
||||||
|
|
|
@ -99,6 +99,9 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
||||||
_isPageAnimatingNotifier.dispose();
|
_isPageAnimatingNotifier.dispose();
|
||||||
_dotLocationNotifier.dispose();
|
_dotLocationNotifier.dispose();
|
||||||
_infoLocationNotifier.dispose();
|
_infoLocationNotifier.dispose();
|
||||||
|
// provided collection should be a new instance specifically created
|
||||||
|
// for the `LocationPickPage` widget, so it can be safely disposed here
|
||||||
|
widget.collection?.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
52
lib/widgets/dialogs/time_shift_dialog.dart
Normal file
52
lib/widgets/dialogs/time_shift_dialog.dart
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import 'package:aves/widgets/common/basic/time_shift_selector.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'aves_dialog.dart';
|
||||||
|
|
||||||
|
class TimeShiftDialog extends StatefulWidget {
|
||||||
|
static const routeName = '/dialog/time_shift';
|
||||||
|
|
||||||
|
final Duration initialValue;
|
||||||
|
|
||||||
|
const TimeShiftDialog({
|
||||||
|
super.key,
|
||||||
|
required this.initialValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TimeShiftDialog> createState() => _TimeShiftDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimeShiftDialogState extends State<TimeShiftDialog> {
|
||||||
|
late TimeShiftController _timeShiftController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_timeShiftController = TimeShiftController(
|
||||||
|
initialValue: widget.initialValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AvesDialog(
|
||||||
|
scrollableContent: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: TimeShiftSelector(controller: _timeShiftController),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
const CancelButton(),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => _submit(context),
|
||||||
|
child: Text(context.l10n.applyButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _submit(BuildContext context) => Navigator.maybeOf(context)?.pop(_timeShiftController.value);
|
||||||
|
}
|
|
@ -50,6 +50,7 @@ class MapPage extends StatelessWidget {
|
||||||
final double? initialZoom;
|
final double? initialZoom;
|
||||||
final AvesEntry? initialEntry;
|
final AvesEntry? initialEntry;
|
||||||
final MappedGeoTiff? overlayEntry;
|
final MappedGeoTiff? overlayEntry;
|
||||||
|
final Set<List<LatLng>>? tracks;
|
||||||
|
|
||||||
const MapPage({
|
const MapPage({
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -58,6 +59,7 @@ class MapPage extends StatelessWidget {
|
||||||
this.initialZoom,
|
this.initialZoom,
|
||||||
this.initialEntry,
|
this.initialEntry,
|
||||||
this.overlayEntry,
|
this.overlayEntry,
|
||||||
|
this.tracks,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -83,6 +85,7 @@ class MapPage extends StatelessWidget {
|
||||||
initialZoom: initialZoom,
|
initialZoom: initialZoom,
|
||||||
initialEntry: initialEntry,
|
initialEntry: initialEntry,
|
||||||
overlayEntry: overlayEntry,
|
overlayEntry: overlayEntry,
|
||||||
|
tracks: tracks,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -96,6 +99,7 @@ class _Content extends StatefulWidget {
|
||||||
final double? initialZoom;
|
final double? initialZoom;
|
||||||
final AvesEntry? initialEntry;
|
final AvesEntry? initialEntry;
|
||||||
final MappedGeoTiff? overlayEntry;
|
final MappedGeoTiff? overlayEntry;
|
||||||
|
final Set<List<LatLng>>? tracks;
|
||||||
|
|
||||||
const _Content({
|
const _Content({
|
||||||
required this.collection,
|
required this.collection,
|
||||||
|
@ -103,6 +107,7 @@ class _Content extends StatefulWidget {
|
||||||
this.initialZoom,
|
this.initialZoom,
|
||||||
this.initialEntry,
|
this.initialEntry,
|
||||||
this.overlayEntry,
|
this.overlayEntry,
|
||||||
|
this.tracks,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -266,6 +271,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMap() {
|
Widget _buildMap() {
|
||||||
|
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||||
final canPop = Navigator.maybeOf(context)?.canPop() == true;
|
final canPop = Navigator.maybeOf(context)?.canPop() == true;
|
||||||
Widget child = MapTheme(
|
Widget child = MapTheme(
|
||||||
interactive: true,
|
interactive: true,
|
||||||
|
@ -285,6 +291,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
||||||
dotLocationNotifier: _dotLocationNotifier,
|
dotLocationNotifier: _dotLocationNotifier,
|
||||||
overlayOpacityNotifier: _overlayOpacityNotifier,
|
overlayOpacityNotifier: _overlayOpacityNotifier,
|
||||||
overlayEntry: widget.overlayEntry,
|
overlayEntry: widget.overlayEntry,
|
||||||
|
tracks: widget.tracks,
|
||||||
onMapTap: (_) => _toggleOverlay(),
|
onMapTap: (_) => _toggleOverlay(),
|
||||||
onMarkerTap: (location, entry) async {
|
onMarkerTap: (location, entry) async {
|
||||||
final index = regionCollection?.sortedEntries.indexOf(entry);
|
final index = regionCollection?.sortedEntries.indexOf(entry);
|
||||||
|
@ -294,7 +301,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
context.read<HighlightInfo>().set(entry);
|
context.read<HighlightInfo>().set(entry);
|
||||||
},
|
},
|
||||||
onMarkerLongPress: _onMarkerLongPress,
|
onMarkerLongPress: appMode.canEditEntry ? _onMarkerLongPress : null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (settings.useTvLayout) {
|
if (settings.useTvLayout) {
|
||||||
|
@ -422,6 +429,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
||||||
void _goToViewer(AvesEntry? initialEntry) {
|
void _goToViewer(AvesEntry? initialEntry) {
|
||||||
if (initialEntry == null) return;
|
if (initialEntry == null) return;
|
||||||
|
|
||||||
|
final appModeNotifier = context.read<ValueNotifier<AppMode>>();
|
||||||
Navigator.maybeOf(context)?.push(
|
Navigator.maybeOf(context)?.push(
|
||||||
TransparentMaterialPageRoute(
|
TransparentMaterialPageRoute(
|
||||||
settings: const RouteSettings(name: EntryViewerPage.routeName),
|
settings: const RouteSettings(name: EntryViewerPage.routeName),
|
||||||
|
@ -429,9 +437,14 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
||||||
final viewerCollection = regionCollection?.copyWith(
|
final viewerCollection = regionCollection?.copyWith(
|
||||||
listenToSource: false,
|
listenToSource: false,
|
||||||
);
|
);
|
||||||
return EntryViewerPage(
|
// propagate app mode from the map page, as it could be locally overridden
|
||||||
|
// and differ from the real app mode above the `Navigator`
|
||||||
|
return ListenableProvider<ValueNotifier<AppMode>>.value(
|
||||||
|
value: appModeNotifier,
|
||||||
|
child: EntryViewerPage(
|
||||||
collection: viewerCollection,
|
collection: viewerCollection,
|
||||||
initialEntry: initialEntry,
|
initialEntry: initialEntry,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -531,7 +544,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
||||||
case MapClusterAction.editLocation:
|
case MapClusterAction.editLocation:
|
||||||
final regionEntries = regionCollection?.sortedEntries ?? [];
|
final regionEntries = regionCollection?.sortedEntries ?? [];
|
||||||
final markerIndex = regionEntries.indexOf(markerEntry);
|
final markerIndex = regionEntries.indexOf(markerEntry);
|
||||||
final location = await delegate.editLocationByMap(context, clusterEntries, markerLocation, openingCollection);
|
final location = await delegate.editLocationByMap(context, clusterEntries, markerLocation, openingCollection.copyWith());
|
||||||
if (location != null) {
|
if (location != null) {
|
||||||
if (markerIndex != -1) {
|
if (markerIndex != -1) {
|
||||||
_selectedIndexNotifier.value = markerIndex;
|
_selectedIndexNotifier.value = markerIndex;
|
||||||
|
|
|
@ -74,14 +74,14 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
case EntryAction.delete:
|
case EntryAction.delete:
|
||||||
case EntryAction.rename:
|
case EntryAction.rename:
|
||||||
case EntryAction.move:
|
case EntryAction.move:
|
||||||
return targetEntry.canEdit;
|
return canWrite && targetEntry.canEdit;
|
||||||
case EntryAction.copy:
|
case EntryAction.copy:
|
||||||
return canWrite;
|
return canWrite;
|
||||||
case EntryAction.rotateCCW:
|
case EntryAction.rotateCCW:
|
||||||
case EntryAction.rotateCW:
|
case EntryAction.rotateCW:
|
||||||
return targetEntry.canRotate;
|
return canWrite && targetEntry.canRotate;
|
||||||
case EntryAction.flip:
|
case EntryAction.flip:
|
||||||
return targetEntry.canFlip;
|
return canWrite && targetEntry.canFlip;
|
||||||
case EntryAction.convert:
|
case EntryAction.convert:
|
||||||
return canWrite && !targetEntry.isPureVideo;
|
return canWrite && !targetEntry.isPureVideo;
|
||||||
case EntryAction.print:
|
case EntryAction.print:
|
||||||
|
|
|
@ -135,10 +135,12 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _editLocation(BuildContext context, AvesEntry targetEntry, CollectionLens? collection) async {
|
Future<void> _editLocation(BuildContext context, AvesEntry targetEntry, CollectionLens? collection) async {
|
||||||
final location = await selectLocation(context, {targetEntry}, collection);
|
final locationByEntry = await selectLocation(context, {targetEntry}, collection);
|
||||||
if (location == null) return;
|
if (locationByEntry == null) return;
|
||||||
|
|
||||||
await edit(context, targetEntry, () => targetEntry.editLocation(location));
|
if (locationByEntry.containsKey(targetEntry)) {
|
||||||
|
await edit(context, targetEntry, () => targetEntry.editLocation(locationByEntry[targetEntry]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _editTitleDescription(BuildContext context, AvesEntry targetEntry) async {
|
Future<void> _editTitleDescription(BuildContext context, AvesEntry targetEntry) async {
|
||||||
|
|
|
@ -3,7 +3,6 @@ import 'dart:math';
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/entry/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/entry/extensions/multipage.dart';
|
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||||
import 'package:aves/model/entry/extensions/props.dart';
|
|
||||||
import 'package:aves/model/settings/enums/accessibility_animations.dart';
|
import 'package:aves/model/settings/enums/accessibility_animations.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
|
@ -232,6 +231,8 @@ class ViewerButtonRowContent extends StatefulWidget {
|
||||||
class _ViewerButtonRowContentState extends State<ViewerButtonRowContent> {
|
class _ViewerButtonRowContentState extends State<ViewerButtonRowContent> {
|
||||||
final ValueNotifier<String?> _popupExpandedNotifier = ValueNotifier(null);
|
final ValueNotifier<String?> _popupExpandedNotifier = ValueNotifier(null);
|
||||||
|
|
||||||
|
EntryActionDelegate get actionDelegate => widget.actionDelegate;
|
||||||
|
|
||||||
AvesEntry get mainEntry => widget.mainEntry;
|
AvesEntry get mainEntry => widget.mainEntry;
|
||||||
|
|
||||||
AvesEntry get pageEntry => widget.pageEntry;
|
AvesEntry get pageEntry => widget.pageEntry;
|
||||||
|
@ -248,10 +249,13 @@ class _ViewerButtonRowContentState extends State<ViewerButtonRowContent> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||||
|
final showOrientationActions = EntryActions.orientationActions.any((v) => actionDelegate.isVisible(appMode: appMode, action: v));
|
||||||
final topLevelActions = widget.topLevelActions;
|
final topLevelActions = widget.topLevelActions;
|
||||||
final exportActions = widget.exportActions;
|
final exportActions = widget.exportActions;
|
||||||
final videoActions = widget.videoActions;
|
final videoActions = widget.videoActions;
|
||||||
final hasOverflowMenu = pageEntry.canRotate || pageEntry.canFlip || topLevelActions.isNotEmpty || exportActions.isNotEmpty || videoActions.isNotEmpty;
|
|
||||||
|
final hasOverflowMenu = showOrientationActions || topLevelActions.isNotEmpty || exportActions.isNotEmpty || videoActions.isNotEmpty;
|
||||||
final animations = context.select<Settings, AccessibilityAnimations>((v) => v.accessibilityAnimations);
|
final animations = context.select<Settings, AccessibilityAnimations>((v) => v.accessibilityAnimations);
|
||||||
return Selector<VideoConductor, AvesVideoController?>(
|
return Selector<VideoConductor, AvesVideoController?>(
|
||||||
selector: (context, vc) => vc.getController(pageEntry),
|
selector: (context, vc) => vc.getController(pageEntry),
|
||||||
|
@ -275,7 +279,7 @@ class _ViewerButtonRowContentState extends State<ViewerButtonRowContent> {
|
||||||
final exportInternalActions = exportActions.whereNot(EntryActions.exportExternal.contains).toList();
|
final exportInternalActions = exportActions.whereNot(EntryActions.exportExternal.contains).toList();
|
||||||
final exportExternalActions = exportActions.where(EntryActions.exportExternal.contains).toList();
|
final exportExternalActions = exportActions.where(EntryActions.exportExternal.contains).toList();
|
||||||
return [
|
return [
|
||||||
if (pageEntry.canRotate || pageEntry.canFlip) _buildRotateAndFlipMenuItems(context),
|
if (showOrientationActions) _buildRotateAndFlipMenuItems(context),
|
||||||
...topLevelActions.map((action) => _buildPopupMenuItem(context, action, videoController)),
|
...topLevelActions.map((action) => _buildPopupMenuItem(context, action, videoController)),
|
||||||
if (exportActions.isNotEmpty)
|
if (exportActions.isNotEmpty)
|
||||||
PopupMenuExpansionPanel<EntryAction>(
|
PopupMenuExpansionPanel<EntryAction>(
|
||||||
|
@ -311,7 +315,7 @@ class _ViewerButtonRowContentState extends State<ViewerButtonRowContent> {
|
||||||
_popupExpandedNotifier.value = null;
|
_popupExpandedNotifier.value = null;
|
||||||
// wait for the popup menu to hide before proceeding with the action
|
// wait for the popup menu to hide before proceeding with the action
|
||||||
await Future.delayed(animations.popUpAnimationDelay * timeDilation);
|
await Future.delayed(animations.popUpAnimationDelay * timeDilation);
|
||||||
widget.actionDelegate.onActionSelected(context, action);
|
actionDelegate.onActionSelected(context, action);
|
||||||
},
|
},
|
||||||
onCanceled: () {
|
onCanceled: () {
|
||||||
_popupExpandedNotifier.value = null;
|
_popupExpandedNotifier.value = null;
|
||||||
|
@ -340,14 +344,14 @@ class _ViewerButtonRowContentState extends State<ViewerButtonRowContent> {
|
||||||
mainEntry: mainEntry,
|
mainEntry: mainEntry,
|
||||||
pageEntry: pageEntry,
|
pageEntry: pageEntry,
|
||||||
videoController: videoController,
|
videoController: videoController,
|
||||||
actionDelegate: widget.actionDelegate,
|
actionDelegate: actionDelegate,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
PopupMenuItem<EntryAction> _buildPopupMenuItem(BuildContext context, EntryAction action, AvesVideoController? videoController) {
|
PopupMenuItem<EntryAction> _buildPopupMenuItem(BuildContext context, EntryAction action, AvesVideoController? videoController) {
|
||||||
var enabled = widget.actionDelegate.canApply(action);
|
var enabled = actionDelegate.canApply(action);
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case EntryAction.videoCaptureFrame:
|
case EntryAction.videoCaptureFrame:
|
||||||
enabled &= videoController?.canCaptureFrameNotifier.value ?? false;
|
enabled &= videoController?.canCaptureFrameNotifier.value ?? false;
|
||||||
|
@ -406,7 +410,7 @@ class _ViewerButtonRowContentState extends State<ViewerButtonRowContent> {
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: PopupMenuItem(
|
child: PopupMenuItem(
|
||||||
value: action,
|
value: action,
|
||||||
enabled: widget.actionDelegate.canApply(action),
|
enabled: actionDelegate.canApply(action),
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
message: action.getText(context),
|
message: action.getText(context),
|
||||||
child: Center(child: action.getIcon()),
|
child: Center(child: action.getIcon()),
|
||||||
|
|
|
@ -25,6 +25,7 @@ class MapThemeData {
|
||||||
static const double markerImageExtent = 48.0;
|
static const double markerImageExtent = 48.0;
|
||||||
static const Size markerArrowSize = Size(8, 6);
|
static const Size markerArrowSize = Size(8, 6);
|
||||||
static const double markerDotDiameter = 16;
|
static const double markerDotDiameter = 16;
|
||||||
|
static const int trackWidth = 5;
|
||||||
|
|
||||||
static Color markerThemedOuterBorderColor(bool isDark) => isDark ? Colors.white30 : Colors.black26;
|
static Color markerThemedOuterBorderColor(bool isDark) => isDark ? Colors.white30 : Colors.black26;
|
||||||
|
|
||||||
|
|
|
@ -91,20 +91,20 @@ class EntryActions {
|
||||||
|
|
||||||
static const pageActions = {
|
static const pageActions = {
|
||||||
EntryAction.videoCaptureFrame,
|
EntryAction.videoCaptureFrame,
|
||||||
EntryAction.videoSelectStreams,
|
EntryAction.videoToggleMute,
|
||||||
EntryAction.videoSetSpeed,
|
EntryAction.videoSetSpeed,
|
||||||
EntryAction.videoABRepeat,
|
EntryAction.videoABRepeat,
|
||||||
EntryAction.videoToggleMute,
|
EntryAction.videoSelectStreams,
|
||||||
EntryAction.videoSettings,
|
EntryAction.videoSettings,
|
||||||
EntryAction.videoTogglePlay,
|
...videoPlayback,
|
||||||
EntryAction.videoReplay10,
|
...orientationActions,
|
||||||
EntryAction.videoSkip10,
|
};
|
||||||
EntryAction.videoShowPreviousFrame,
|
|
||||||
EntryAction.videoShowNextFrame,
|
static const orientationActions = [
|
||||||
EntryAction.rotateCCW,
|
EntryAction.rotateCCW,
|
||||||
EntryAction.rotateCW,
|
EntryAction.rotateCW,
|
||||||
EntryAction.flip,
|
EntryAction.flip,
|
||||||
};
|
];
|
||||||
|
|
||||||
static const trashed = [
|
static const trashed = [
|
||||||
EntryAction.delete,
|
EntryAction.delete,
|
||||||
|
|
|
@ -23,6 +23,7 @@ enum LocationEditAction {
|
||||||
chooseOnMap,
|
chooseOnMap,
|
||||||
copyItem,
|
copyItem,
|
||||||
setCustom,
|
setCustom,
|
||||||
|
importGpx,
|
||||||
remove,
|
remove,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ abstract class MobileServices {
|
||||||
required ValueNotifier<LatLng?>? dotLocationNotifier,
|
required ValueNotifier<LatLng?>? dotLocationNotifier,
|
||||||
required ValueNotifier<double>? overlayOpacityNotifier,
|
required ValueNotifier<double>? overlayOpacityNotifier,
|
||||||
required MapOverlay? overlayEntry,
|
required MapOverlay? overlayEntry,
|
||||||
|
required Set<List<LatLng>>? tracks,
|
||||||
required UserZoomChangeCallback? onUserZoomChange,
|
required UserZoomChangeCallback? onUserZoomChange,
|
||||||
required MapTapCallback? onMapTap,
|
required MapTapCallback? onMapTap,
|
||||||
required MarkerTapCallback<T>? onMarkerTap,
|
required MarkerTapCallback<T>? onMarkerTap,
|
||||||
|
|
|
@ -58,6 +58,7 @@ class PlatformMobileServices extends MobileServices {
|
||||||
required ValueNotifier<ll.LatLng?>? dotLocationNotifier,
|
required ValueNotifier<ll.LatLng?>? dotLocationNotifier,
|
||||||
required ValueNotifier<double>? overlayOpacityNotifier,
|
required ValueNotifier<double>? overlayOpacityNotifier,
|
||||||
required MapOverlay? overlayEntry,
|
required MapOverlay? overlayEntry,
|
||||||
|
required Set<List<ll.LatLng>>? tracks,
|
||||||
required UserZoomChangeCallback? onUserZoomChange,
|
required UserZoomChangeCallback? onUserZoomChange,
|
||||||
required MapTapCallback? onMapTap,
|
required MapTapCallback? onMapTap,
|
||||||
required MarkerTapCallback<T>? onMarkerTap,
|
required MarkerTapCallback<T>? onMarkerTap,
|
||||||
|
@ -78,6 +79,7 @@ class PlatformMobileServices extends MobileServices {
|
||||||
dotLocationNotifier: dotLocationNotifier,
|
dotLocationNotifier: dotLocationNotifier,
|
||||||
overlayOpacityNotifier: overlayOpacityNotifier,
|
overlayOpacityNotifier: overlayOpacityNotifier,
|
||||||
overlayEntry: overlayEntry,
|
overlayEntry: overlayEntry,
|
||||||
|
tracks: tracks,
|
||||||
onUserZoomChange: onUserZoomChange,
|
onUserZoomChange: onUserZoomChange,
|
||||||
onMapTap: onMapTap,
|
onMapTap: onMapTap,
|
||||||
onMarkerTap: onMarkerTap,
|
onMarkerTap: onMarkerTap,
|
||||||
|
|
|
@ -22,6 +22,7 @@ class EntryGoogleMap<T> extends StatefulWidget {
|
||||||
final ValueNotifier<ll.LatLng?>? dotLocationNotifier;
|
final ValueNotifier<ll.LatLng?>? dotLocationNotifier;
|
||||||
final ValueNotifier<double>? overlayOpacityNotifier;
|
final ValueNotifier<double>? overlayOpacityNotifier;
|
||||||
final MapOverlay? overlayEntry;
|
final MapOverlay? overlayEntry;
|
||||||
|
final Set<List<ll.LatLng>>? tracks;
|
||||||
final UserZoomChangeCallback? onUserZoomChange;
|
final UserZoomChangeCallback? onUserZoomChange;
|
||||||
final MapTapCallback? onMapTap;
|
final MapTapCallback? onMapTap;
|
||||||
final MarkerTapCallback<T>? onMarkerTap;
|
final MarkerTapCallback<T>? onMarkerTap;
|
||||||
|
@ -43,6 +44,7 @@ class EntryGoogleMap<T> extends StatefulWidget {
|
||||||
required this.dotLocationNotifier,
|
required this.dotLocationNotifier,
|
||||||
this.overlayOpacityNotifier,
|
this.overlayOpacityNotifier,
|
||||||
this.overlayEntry,
|
this.overlayEntry,
|
||||||
|
this.tracks,
|
||||||
this.onUserZoomChange,
|
this.onUserZoomChange,
|
||||||
this.onMapTap,
|
this.onMapTap,
|
||||||
this.onMarkerTap,
|
this.onMarkerTap,
|
||||||
|
@ -164,6 +166,8 @@ class _EntryGoogleMapState<T> extends State<EntryGoogleMap<T>> {
|
||||||
|
|
||||||
final interactive = context.select<MapThemeData, bool>((v) => v.interactive);
|
final interactive = context.select<MapThemeData, bool>((v) => v.interactive);
|
||||||
final overlayEntry = widget.overlayEntry;
|
final overlayEntry = widget.overlayEntry;
|
||||||
|
final tracks = widget.tracks;
|
||||||
|
final trackColor = Theme.of(context).colorScheme.primary;
|
||||||
return NullableValueListenableBuilder<ll.LatLng?>(
|
return NullableValueListenableBuilder<ll.LatLng?>(
|
||||||
valueListenable: widget.dotLocationNotifier,
|
valueListenable: widget.dotLocationNotifier,
|
||||||
builder: (context, dotLocation, child) {
|
builder: (context, dotLocation, child) {
|
||||||
|
@ -208,6 +212,16 @@ class _EntryGoogleMapState<T> extends State<EntryGoogleMap<T>> {
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
polylines: {
|
||||||
|
if (tracks != null)
|
||||||
|
for (final track in tracks)
|
||||||
|
Polyline(
|
||||||
|
polylineId: PolylineId(track.hashCode.toString()),
|
||||||
|
points: track.map(_toServiceLatLng).toList(),
|
||||||
|
width: MapThemeData.trackWidth,
|
||||||
|
color: trackColor,
|
||||||
|
),
|
||||||
|
},
|
||||||
// TODO TLAD [geotiff] may use ground overlay instead when this is fixed: https://github.com/flutter/flutter/issues/26479
|
// TODO TLAD [geotiff] may use ground overlay instead when this is fixed: https://github.com/flutter/flutter/issues/26479
|
||||||
tileOverlays: {
|
tileOverlays: {
|
||||||
if (overlayEntry != null && overlayEntry.canOverlay)
|
if (overlayEntry != null && overlayEntry.canOverlay)
|
||||||
|
@ -331,6 +345,7 @@ class _GoogleMap extends StatefulWidget {
|
||||||
final MinMaxZoomPreference minMaxZoomPreference;
|
final MinMaxZoomPreference minMaxZoomPreference;
|
||||||
final bool interactive;
|
final bool interactive;
|
||||||
final Set<Marker> markers;
|
final Set<Marker> markers;
|
||||||
|
final Set<Polyline> polylines;
|
||||||
final Set<TileOverlay> tileOverlays;
|
final Set<TileOverlay> tileOverlays;
|
||||||
final CameraPositionCallback? onCameraMove;
|
final CameraPositionCallback? onCameraMove;
|
||||||
final VoidCallback? onCameraIdle;
|
final VoidCallback? onCameraIdle;
|
||||||
|
@ -344,6 +359,7 @@ class _GoogleMap extends StatefulWidget {
|
||||||
required this.minMaxZoomPreference,
|
required this.minMaxZoomPreference,
|
||||||
required this.interactive,
|
required this.interactive,
|
||||||
required this.markers,
|
required this.markers,
|
||||||
|
required this.polylines,
|
||||||
required this.tileOverlays,
|
required this.tileOverlays,
|
||||||
required this.onCameraMove,
|
required this.onCameraMove,
|
||||||
required this.onCameraIdle,
|
required this.onCameraIdle,
|
||||||
|
@ -414,6 +430,7 @@ class _GoogleMapState extends State<_GoogleMap> {
|
||||||
myLocationEnabled: false,
|
myLocationEnabled: false,
|
||||||
myLocationButtonEnabled: false,
|
myLocationButtonEnabled: false,
|
||||||
markers: widget.markers,
|
markers: widget.markers,
|
||||||
|
polylines: widget.polylines,
|
||||||
tileOverlays: widget.tileOverlays,
|
tileOverlays: widget.tileOverlays,
|
||||||
onCameraMove: widget.onCameraMove,
|
onCameraMove: widget.onCameraMove,
|
||||||
onCameraIdle: widget.onCameraIdle,
|
onCameraIdle: widget.onCameraIdle,
|
||||||
|
|
|
@ -30,6 +30,7 @@ class PlatformMobileServices extends MobileServices {
|
||||||
required ValueNotifier<LatLng?>? dotLocationNotifier,
|
required ValueNotifier<LatLng?>? dotLocationNotifier,
|
||||||
required ValueNotifier<double>? overlayOpacityNotifier,
|
required ValueNotifier<double>? overlayOpacityNotifier,
|
||||||
required MapOverlay? overlayEntry,
|
required MapOverlay? overlayEntry,
|
||||||
|
required Set<List<LatLng>>? tracks,
|
||||||
required UserZoomChangeCallback? onUserZoomChange,
|
required UserZoomChangeCallback? onUserZoomChange,
|
||||||
required MapTapCallback? onMapTap,
|
required MapTapCallback? onMapTap,
|
||||||
required MarkerTapCallback<T>? onMarkerTap,
|
required MarkerTapCallback<T>? onMarkerTap,
|
||||||
|
|
16
pubspec.lock
16
pubspec.lock
|
@ -576,6 +576,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.10"
|
version: "0.5.10"
|
||||||
|
gpx:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: gpx
|
||||||
|
sha256: f5b12b86402c639079243600ee2b3afd85cd08d26117fc8885cf48efce471d8e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
highlight:
|
highlight:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1188,6 +1196,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.2"
|
||||||
|
quiver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quiver
|
||||||
|
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
safe_local_storage:
|
safe_local_storage:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -90,6 +90,7 @@ dependencies:
|
||||||
flutter_markdown:
|
flutter_markdown:
|
||||||
flutter_staggered_animations:
|
flutter_staggered_animations:
|
||||||
get_it:
|
get_it:
|
||||||
|
gpx:
|
||||||
http:
|
http:
|
||||||
intl:
|
intl:
|
||||||
latlong2:
|
latlong2:
|
||||||
|
|
Loading…
Reference in a new issue