diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_controller.g.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_controller.g.dart index f2f8f7df..de0745b7 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_controller.g.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_controller.g.dart @@ -34,7 +34,7 @@ final class LocationControllerProvider } String _$locationControllerHash() => - r'43534cd92b74ec5fabc9a43c6ef8398846855cf4'; + r'324461865e3f1deb94ef6bba29cb3a200e5fe14f'; abstract class _$LocationController extends $AsyncNotifier { FutureOr build(); diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_map_controller.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_map_controller.dart index 1f5603ff..204a836d 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_map_controller.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_map_controller.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ui'; import 'package:latlong2/latlong.dart'; import 'package:legacy_device_state/legacy_device_state.dart'; @@ -15,12 +16,22 @@ class LocationMapController extends _$LocationMapController { late final SfTrackingRepository _tracking; Timer? _zoomDebounce; + Timer? _playbackTimer; + Timer? _recenterTimer; static const Duration _zoomDebounceDelay = Duration(seconds: 1); + static const int _playbackSeconds = 3; + static const int _recenterSeconds = 3; + + VoidCallback? onPlaybackTick; @override LocationMapState build() { _tracking = ref.read(sfTrackingProvider); - ref.onDispose(() => _zoomDebounce?.cancel()); + ref.onDispose(() { + _zoomDebounce?.cancel(); + _playbackTimer?.cancel(); + _recenterTimer?.cancel(); + }); return const LocationMapState(); } @@ -56,6 +67,8 @@ class LocationMapController extends _$LocationMapController { placingMode: PlacingMode.none, adjustingRadius: false, previewPoint: null, + editingGeofence: null, + editingFrequentPlace: null, ); } @@ -75,13 +88,17 @@ class LocationMapController extends _$LocationMapController { placingMode: PlacingMode.none, adjustingRadius: true, previewPoint: center, - previewRadius: 200, + previewRadius: state.editingGeofence?.radius ?? 200, ); } void confirmFrequentPlacePlacement() { unawaited(_tracking.legacyLocationPointConfirmed('frequent_place')); - state = state.copyWith(placingMode: PlacingMode.none, previewPoint: null); + state = state.copyWith( + placingMode: PlacingMode.none, + previewPoint: null, + editingFrequentPlace: null, + ); } void updatePreviewRadius(double radius) { @@ -123,9 +140,19 @@ class LocationMapController extends _$LocationMapController { state = state.copyWith( selectedGeofence: null, editingGeofence: geofence, + placingMode: PlacingMode.geofence, previewPoint: LatLng(geofence.latitude, geofence.longitude), previewRadius: geofence.radius, - adjustingRadius: true, + ); + } + + void startEditingFrequentPlace(FrequentPlaceEntity place) { + unawaited(_tracking.legacyLocationFrequentPlaceSelected()); + state = state.copyWith( + selectedFrequentPlace: null, + editingFrequentPlace: place, + placingMode: PlacingMode.frequentPlace, + previewPoint: LatLng(place.lat, place.lng), ); } @@ -191,6 +218,96 @@ class LocationMapController extends _$LocationMapController { }); } + void startReveal() { + state = state.copyWith(isRevealAnimating: true, historyRevealCount: 1); + } + + void updateRevealCount(int count) { + state = state.copyWith(historyRevealCount: count); + } + + void finishReveal() { + state = state.copyWith(isRevealAnimating: false, historyRevealCount: null); + } + + void startHistoryNavigation() { + state = state.copyWith(historyNavigationIndex: 0, historyPlaying: false); + } + + void stopHistoryNavigation() { + state = state.copyWith(historyNavigationIndex: -1, historyPlaying: false); + } + + void setHistoryIndex(int index) { + state = state.copyWith(historyNavigationIndex: index); + } + + void nextHistoryPosition(int total) { + if (state.historyNavigationIndex < total - 1) { + state = state.copyWith( + historyNavigationIndex: state.historyNavigationIndex + 1, + ); + } else { + state = state.copyWith(historyPlaying: false); + } + } + + void previousHistoryPosition() { + if (state.historyNavigationIndex > 0) { + state = state.copyWith( + historyNavigationIndex: state.historyNavigationIndex - 1, + ); + } + } + + void toggleHistoryPlayback(int total) { + if (state.historyPlaying) { + stopPlaybackTimer(); + state = state.copyWith(historyPlaying: false); + } else { + final atEnd = state.historyNavigationIndex >= total - 1; + state = state.copyWith( + historyNavigationIndex: atEnd ? 0 : state.historyNavigationIndex, + historyPlaying: true, + ); + startPlaybackTimer(total); + } + } + + void startPlaybackTimer(int total) { + _playbackTimer?.cancel(); + _playbackTimer = Timer.periodic(const Duration(seconds: _playbackSeconds), ( + _, + ) { + if (!state.historyPlaying) { + _playbackTimer?.cancel(); + return; + } + nextHistoryPosition(total); + onPlaybackTick?.call(); + if (!state.historyPlaying) { + _playbackTimer?.cancel(); + } + }); + } + + void stopPlaybackTimer() { + _playbackTimer?.cancel(); + _playbackTimer = null; + } + + void scheduleRecenter(VoidCallback onRecenter) { + if (!state.isFollowing) return; + _recenterTimer?.cancel(); + _recenterTimer = Timer(const Duration(seconds: _recenterSeconds), () { + if (state.isFollowing) onRecenter(); + }); + } + + void cancelRecenter() { + _recenterTimer?.cancel(); + } + String _modeName(PlacingMode mode) { switch (mode) { case PlacingMode.geofence: diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_map_controller.g.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_map_controller.g.dart index 5678c5ed..566a16c0 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_map_controller.g.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_map_controller.g.dart @@ -42,7 +42,7 @@ final class LocationMapControllerProvider } String _$locationMapControllerHash() => - r'c6eea4cec7a9a66546e9b66baf384edbb6e320f2'; + r'565ab5147dd5ac16cf80210c55e28316bafe2f5a'; abstract class _$LocationMapController extends $Notifier { LocationMapState build(); diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_map_state.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_map_state.dart index 25a0ae00..59f85d79 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_map_state.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_map_state.dart @@ -22,10 +22,15 @@ abstract class LocationMapState with _$LocationMapState { GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, + FrequentPlaceEntity? editingFrequentPlace, PositionEntity? selectedHistoryPosition, @Default(false) bool isFollowing, @Default(false) bool actionsExpanded, @Default(false) bool frequencyExpanded, @Default(_defaultZoom) double mapZoom, + @Default(-1) int historyNavigationIndex, + @Default(false) bool historyPlaying, + @Default(false) bool isRevealAnimating, + int? historyRevealCount, }) = _LocationMapState; } diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_map_state.freezed.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_map_state.freezed.dart index 2b2b1748..04869616 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_map_state.freezed.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/providers/location_map_state.freezed.dart @@ -14,7 +14,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$LocationMapState { - bool get showGeofences; bool get showFrequentPlaces; PlacingMode get placingMode; bool get adjustingRadius; double get previewRadius; LatLng? get previewPoint; GeofenceEntity? get selectedGeofence; GeofenceEntity? get editingGeofence; FrequentPlaceEntity? get selectedFrequentPlace; PositionEntity? get selectedHistoryPosition; bool get isFollowing; bool get actionsExpanded; bool get frequencyExpanded; double get mapZoom; + bool get showGeofences; bool get showFrequentPlaces; PlacingMode get placingMode; bool get adjustingRadius; double get previewRadius; LatLng? get previewPoint; GeofenceEntity? get selectedGeofence; GeofenceEntity? get editingGeofence; FrequentPlaceEntity? get selectedFrequentPlace; FrequentPlaceEntity? get editingFrequentPlace; PositionEntity? get selectedHistoryPosition; bool get isFollowing; bool get actionsExpanded; bool get frequencyExpanded; double get mapZoom; int get historyNavigationIndex; bool get historyPlaying; bool get isRevealAnimating; int? get historyRevealCount; /// Create a copy of LocationMapState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -25,16 +25,16 @@ $LocationMapStateCopyWith get copyWith => _$LocationMapStateCo @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is LocationMapState&&(identical(other.showGeofences, showGeofences) || other.showGeofences == showGeofences)&&(identical(other.showFrequentPlaces, showFrequentPlaces) || other.showFrequentPlaces == showFrequentPlaces)&&(identical(other.placingMode, placingMode) || other.placingMode == placingMode)&&(identical(other.adjustingRadius, adjustingRadius) || other.adjustingRadius == adjustingRadius)&&(identical(other.previewRadius, previewRadius) || other.previewRadius == previewRadius)&&(identical(other.previewPoint, previewPoint) || other.previewPoint == previewPoint)&&(identical(other.selectedGeofence, selectedGeofence) || other.selectedGeofence == selectedGeofence)&&(identical(other.editingGeofence, editingGeofence) || other.editingGeofence == editingGeofence)&&(identical(other.selectedFrequentPlace, selectedFrequentPlace) || other.selectedFrequentPlace == selectedFrequentPlace)&&(identical(other.selectedHistoryPosition, selectedHistoryPosition) || other.selectedHistoryPosition == selectedHistoryPosition)&&(identical(other.isFollowing, isFollowing) || other.isFollowing == isFollowing)&&(identical(other.actionsExpanded, actionsExpanded) || other.actionsExpanded == actionsExpanded)&&(identical(other.frequencyExpanded, frequencyExpanded) || other.frequencyExpanded == frequencyExpanded)&&(identical(other.mapZoom, mapZoom) || other.mapZoom == mapZoom)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is LocationMapState&&(identical(other.showGeofences, showGeofences) || other.showGeofences == showGeofences)&&(identical(other.showFrequentPlaces, showFrequentPlaces) || other.showFrequentPlaces == showFrequentPlaces)&&(identical(other.placingMode, placingMode) || other.placingMode == placingMode)&&(identical(other.adjustingRadius, adjustingRadius) || other.adjustingRadius == adjustingRadius)&&(identical(other.previewRadius, previewRadius) || other.previewRadius == previewRadius)&&(identical(other.previewPoint, previewPoint) || other.previewPoint == previewPoint)&&(identical(other.selectedGeofence, selectedGeofence) || other.selectedGeofence == selectedGeofence)&&(identical(other.editingGeofence, editingGeofence) || other.editingGeofence == editingGeofence)&&(identical(other.selectedFrequentPlace, selectedFrequentPlace) || other.selectedFrequentPlace == selectedFrequentPlace)&&(identical(other.editingFrequentPlace, editingFrequentPlace) || other.editingFrequentPlace == editingFrequentPlace)&&(identical(other.selectedHistoryPosition, selectedHistoryPosition) || other.selectedHistoryPosition == selectedHistoryPosition)&&(identical(other.isFollowing, isFollowing) || other.isFollowing == isFollowing)&&(identical(other.actionsExpanded, actionsExpanded) || other.actionsExpanded == actionsExpanded)&&(identical(other.frequencyExpanded, frequencyExpanded) || other.frequencyExpanded == frequencyExpanded)&&(identical(other.mapZoom, mapZoom) || other.mapZoom == mapZoom)&&(identical(other.historyNavigationIndex, historyNavigationIndex) || other.historyNavigationIndex == historyNavigationIndex)&&(identical(other.historyPlaying, historyPlaying) || other.historyPlaying == historyPlaying)&&(identical(other.isRevealAnimating, isRevealAnimating) || other.isRevealAnimating == isRevealAnimating)&&(identical(other.historyRevealCount, historyRevealCount) || other.historyRevealCount == historyRevealCount)); } @override -int get hashCode => Object.hash(runtimeType,showGeofences,showFrequentPlaces,placingMode,adjustingRadius,previewRadius,previewPoint,selectedGeofence,editingGeofence,selectedFrequentPlace,selectedHistoryPosition,isFollowing,actionsExpanded,frequencyExpanded,mapZoom); +int get hashCode => Object.hashAll([runtimeType,showGeofences,showFrequentPlaces,placingMode,adjustingRadius,previewRadius,previewPoint,selectedGeofence,editingGeofence,selectedFrequentPlace,editingFrequentPlace,selectedHistoryPosition,isFollowing,actionsExpanded,frequencyExpanded,mapZoom,historyNavigationIndex,historyPlaying,isRevealAnimating,historyRevealCount]); @override String toString() { - return 'LocationMapState(showGeofences: $showGeofences, showFrequentPlaces: $showFrequentPlaces, placingMode: $placingMode, adjustingRadius: $adjustingRadius, previewRadius: $previewRadius, previewPoint: $previewPoint, selectedGeofence: $selectedGeofence, editingGeofence: $editingGeofence, selectedFrequentPlace: $selectedFrequentPlace, selectedHistoryPosition: $selectedHistoryPosition, isFollowing: $isFollowing, actionsExpanded: $actionsExpanded, frequencyExpanded: $frequencyExpanded, mapZoom: $mapZoom)'; + return 'LocationMapState(showGeofences: $showGeofences, showFrequentPlaces: $showFrequentPlaces, placingMode: $placingMode, adjustingRadius: $adjustingRadius, previewRadius: $previewRadius, previewPoint: $previewPoint, selectedGeofence: $selectedGeofence, editingGeofence: $editingGeofence, selectedFrequentPlace: $selectedFrequentPlace, editingFrequentPlace: $editingFrequentPlace, selectedHistoryPosition: $selectedHistoryPosition, isFollowing: $isFollowing, actionsExpanded: $actionsExpanded, frequencyExpanded: $frequencyExpanded, mapZoom: $mapZoom, historyNavigationIndex: $historyNavigationIndex, historyPlaying: $historyPlaying, isRevealAnimating: $isRevealAnimating, historyRevealCount: $historyRevealCount)'; } @@ -45,11 +45,11 @@ abstract mixin class $LocationMapStateCopyWith<$Res> { factory $LocationMapStateCopyWith(LocationMapState value, $Res Function(LocationMapState) _then) = _$LocationMapStateCopyWithImpl; @useResult $Res call({ - bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom + bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, FrequentPlaceEntity? editingFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom, int historyNavigationIndex, bool historyPlaying, bool isRevealAnimating, int? historyRevealCount }); -$GeofenceEntityCopyWith<$Res>? get selectedGeofence;$GeofenceEntityCopyWith<$Res>? get editingGeofence;$FrequentPlaceEntityCopyWith<$Res>? get selectedFrequentPlace;$PositionEntityCopyWith<$Res>? get selectedHistoryPosition; +$GeofenceEntityCopyWith<$Res>? get selectedGeofence;$GeofenceEntityCopyWith<$Res>? get editingGeofence;$FrequentPlaceEntityCopyWith<$Res>? get selectedFrequentPlace;$FrequentPlaceEntityCopyWith<$Res>? get editingFrequentPlace;$PositionEntityCopyWith<$Res>? get selectedHistoryPosition; } /// @nodoc @@ -62,7 +62,7 @@ class _$LocationMapStateCopyWithImpl<$Res> /// Create a copy of LocationMapState /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? showGeofences = null,Object? showFrequentPlaces = null,Object? placingMode = null,Object? adjustingRadius = null,Object? previewRadius = null,Object? previewPoint = freezed,Object? selectedGeofence = freezed,Object? editingGeofence = freezed,Object? selectedFrequentPlace = freezed,Object? selectedHistoryPosition = freezed,Object? isFollowing = null,Object? actionsExpanded = null,Object? frequencyExpanded = null,Object? mapZoom = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? showGeofences = null,Object? showFrequentPlaces = null,Object? placingMode = null,Object? adjustingRadius = null,Object? previewRadius = null,Object? previewPoint = freezed,Object? selectedGeofence = freezed,Object? editingGeofence = freezed,Object? selectedFrequentPlace = freezed,Object? editingFrequentPlace = freezed,Object? selectedHistoryPosition = freezed,Object? isFollowing = null,Object? actionsExpanded = null,Object? frequencyExpanded = null,Object? mapZoom = null,Object? historyNavigationIndex = null,Object? historyPlaying = null,Object? isRevealAnimating = null,Object? historyRevealCount = freezed,}) { return _then(_self.copyWith( showGeofences: null == showGeofences ? _self.showGeofences : showGeofences // ignore: cast_nullable_to_non_nullable as bool,showFrequentPlaces: null == showFrequentPlaces ? _self.showFrequentPlaces : showFrequentPlaces // ignore: cast_nullable_to_non_nullable @@ -73,12 +73,17 @@ as double,previewPoint: freezed == previewPoint ? _self.previewPoint : previewPo as LatLng?,selectedGeofence: freezed == selectedGeofence ? _self.selectedGeofence : selectedGeofence // ignore: cast_nullable_to_non_nullable as GeofenceEntity?,editingGeofence: freezed == editingGeofence ? _self.editingGeofence : editingGeofence // ignore: cast_nullable_to_non_nullable as GeofenceEntity?,selectedFrequentPlace: freezed == selectedFrequentPlace ? _self.selectedFrequentPlace : selectedFrequentPlace // ignore: cast_nullable_to_non_nullable +as FrequentPlaceEntity?,editingFrequentPlace: freezed == editingFrequentPlace ? _self.editingFrequentPlace : editingFrequentPlace // ignore: cast_nullable_to_non_nullable as FrequentPlaceEntity?,selectedHistoryPosition: freezed == selectedHistoryPosition ? _self.selectedHistoryPosition : selectedHistoryPosition // ignore: cast_nullable_to_non_nullable as PositionEntity?,isFollowing: null == isFollowing ? _self.isFollowing : isFollowing // ignore: cast_nullable_to_non_nullable as bool,actionsExpanded: null == actionsExpanded ? _self.actionsExpanded : actionsExpanded // ignore: cast_nullable_to_non_nullable as bool,frequencyExpanded: null == frequencyExpanded ? _self.frequencyExpanded : frequencyExpanded // ignore: cast_nullable_to_non_nullable as bool,mapZoom: null == mapZoom ? _self.mapZoom : mapZoom // ignore: cast_nullable_to_non_nullable -as double, +as double,historyNavigationIndex: null == historyNavigationIndex ? _self.historyNavigationIndex : historyNavigationIndex // ignore: cast_nullable_to_non_nullable +as int,historyPlaying: null == historyPlaying ? _self.historyPlaying : historyPlaying // ignore: cast_nullable_to_non_nullable +as bool,isRevealAnimating: null == isRevealAnimating ? _self.isRevealAnimating : isRevealAnimating // ignore: cast_nullable_to_non_nullable +as bool,historyRevealCount: freezed == historyRevealCount ? _self.historyRevealCount : historyRevealCount // ignore: cast_nullable_to_non_nullable +as int?, )); } /// Create a copy of LocationMapState @@ -121,6 +126,18 @@ $FrequentPlaceEntityCopyWith<$Res>? get selectedFrequentPlace { /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') +$FrequentPlaceEntityCopyWith<$Res>? get editingFrequentPlace { + if (_self.editingFrequentPlace == null) { + return null; + } + + return $FrequentPlaceEntityCopyWith<$Res>(_self.editingFrequentPlace!, (value) { + return _then(_self.copyWith(editingFrequentPlace: value)); + }); +}/// Create a copy of LocationMapState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') $PositionEntityCopyWith<$Res>? get selectedHistoryPosition { if (_self.selectedHistoryPosition == null) { return null; @@ -211,10 +228,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, FrequentPlaceEntity? editingFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom, int historyNavigationIndex, bool historyPlaying, bool isRevealAnimating, int? historyRevealCount)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _LocationMapState() when $default != null: -return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_that.adjustingRadius,_that.previewRadius,_that.previewPoint,_that.selectedGeofence,_that.editingGeofence,_that.selectedFrequentPlace,_that.selectedHistoryPosition,_that.isFollowing,_that.actionsExpanded,_that.frequencyExpanded,_that.mapZoom);case _: +return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_that.adjustingRadius,_that.previewRadius,_that.previewPoint,_that.selectedGeofence,_that.editingGeofence,_that.selectedFrequentPlace,_that.editingFrequentPlace,_that.selectedHistoryPosition,_that.isFollowing,_that.actionsExpanded,_that.frequencyExpanded,_that.mapZoom,_that.historyNavigationIndex,_that.historyPlaying,_that.isRevealAnimating,_that.historyRevealCount);case _: return orElse(); } @@ -232,10 +249,10 @@ return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_ /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, FrequentPlaceEntity? editingFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom, int historyNavigationIndex, bool historyPlaying, bool isRevealAnimating, int? historyRevealCount) $default,) {final _that = this; switch (_that) { case _LocationMapState(): -return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_that.adjustingRadius,_that.previewRadius,_that.previewPoint,_that.selectedGeofence,_that.editingGeofence,_that.selectedFrequentPlace,_that.selectedHistoryPosition,_that.isFollowing,_that.actionsExpanded,_that.frequencyExpanded,_that.mapZoom);case _: +return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_that.adjustingRadius,_that.previewRadius,_that.previewPoint,_that.selectedGeofence,_that.editingGeofence,_that.selectedFrequentPlace,_that.editingFrequentPlace,_that.selectedHistoryPosition,_that.isFollowing,_that.actionsExpanded,_that.frequencyExpanded,_that.mapZoom,_that.historyNavigationIndex,_that.historyPlaying,_that.isRevealAnimating,_that.historyRevealCount);case _: throw StateError('Unexpected subclass'); } @@ -252,10 +269,10 @@ return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_ /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, FrequentPlaceEntity? editingFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom, int historyNavigationIndex, bool historyPlaying, bool isRevealAnimating, int? historyRevealCount)? $default,) {final _that = this; switch (_that) { case _LocationMapState() when $default != null: -return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_that.adjustingRadius,_that.previewRadius,_that.previewPoint,_that.selectedGeofence,_that.editingGeofence,_that.selectedFrequentPlace,_that.selectedHistoryPosition,_that.isFollowing,_that.actionsExpanded,_that.frequencyExpanded,_that.mapZoom);case _: +return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_that.adjustingRadius,_that.previewRadius,_that.previewPoint,_that.selectedGeofence,_that.editingGeofence,_that.selectedFrequentPlace,_that.editingFrequentPlace,_that.selectedHistoryPosition,_that.isFollowing,_that.actionsExpanded,_that.frequencyExpanded,_that.mapZoom,_that.historyNavigationIndex,_that.historyPlaying,_that.isRevealAnimating,_that.historyRevealCount);case _: return null; } @@ -267,7 +284,7 @@ return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_ class _LocationMapState implements LocationMapState { - const _LocationMapState({this.showGeofences = true, this.showFrequentPlaces = true, this.placingMode = PlacingMode.none, this.adjustingRadius = false, this.previewRadius = 200.0, this.previewPoint, this.selectedGeofence, this.editingGeofence, this.selectedFrequentPlace, this.selectedHistoryPosition, this.isFollowing = false, this.actionsExpanded = false, this.frequencyExpanded = false, this.mapZoom = _defaultZoom}); + const _LocationMapState({this.showGeofences = true, this.showFrequentPlaces = true, this.placingMode = PlacingMode.none, this.adjustingRadius = false, this.previewRadius = 200.0, this.previewPoint, this.selectedGeofence, this.editingGeofence, this.selectedFrequentPlace, this.editingFrequentPlace, this.selectedHistoryPosition, this.isFollowing = false, this.actionsExpanded = false, this.frequencyExpanded = false, this.mapZoom = _defaultZoom, this.historyNavigationIndex = -1, this.historyPlaying = false, this.isRevealAnimating = false, this.historyRevealCount}); @override@JsonKey() final bool showGeofences; @@ -279,11 +296,16 @@ class _LocationMapState implements LocationMapState { @override final GeofenceEntity? selectedGeofence; @override final GeofenceEntity? editingGeofence; @override final FrequentPlaceEntity? selectedFrequentPlace; +@override final FrequentPlaceEntity? editingFrequentPlace; @override final PositionEntity? selectedHistoryPosition; @override@JsonKey() final bool isFollowing; @override@JsonKey() final bool actionsExpanded; @override@JsonKey() final bool frequencyExpanded; @override@JsonKey() final double mapZoom; +@override@JsonKey() final int historyNavigationIndex; +@override@JsonKey() final bool historyPlaying; +@override@JsonKey() final bool isRevealAnimating; +@override final int? historyRevealCount; /// Create a copy of LocationMapState /// with the given fields replaced by the non-null parameter values. @@ -295,16 +317,16 @@ _$LocationMapStateCopyWith<_LocationMapState> get copyWith => __$LocationMapStat @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _LocationMapState&&(identical(other.showGeofences, showGeofences) || other.showGeofences == showGeofences)&&(identical(other.showFrequentPlaces, showFrequentPlaces) || other.showFrequentPlaces == showFrequentPlaces)&&(identical(other.placingMode, placingMode) || other.placingMode == placingMode)&&(identical(other.adjustingRadius, adjustingRadius) || other.adjustingRadius == adjustingRadius)&&(identical(other.previewRadius, previewRadius) || other.previewRadius == previewRadius)&&(identical(other.previewPoint, previewPoint) || other.previewPoint == previewPoint)&&(identical(other.selectedGeofence, selectedGeofence) || other.selectedGeofence == selectedGeofence)&&(identical(other.editingGeofence, editingGeofence) || other.editingGeofence == editingGeofence)&&(identical(other.selectedFrequentPlace, selectedFrequentPlace) || other.selectedFrequentPlace == selectedFrequentPlace)&&(identical(other.selectedHistoryPosition, selectedHistoryPosition) || other.selectedHistoryPosition == selectedHistoryPosition)&&(identical(other.isFollowing, isFollowing) || other.isFollowing == isFollowing)&&(identical(other.actionsExpanded, actionsExpanded) || other.actionsExpanded == actionsExpanded)&&(identical(other.frequencyExpanded, frequencyExpanded) || other.frequencyExpanded == frequencyExpanded)&&(identical(other.mapZoom, mapZoom) || other.mapZoom == mapZoom)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _LocationMapState&&(identical(other.showGeofences, showGeofences) || other.showGeofences == showGeofences)&&(identical(other.showFrequentPlaces, showFrequentPlaces) || other.showFrequentPlaces == showFrequentPlaces)&&(identical(other.placingMode, placingMode) || other.placingMode == placingMode)&&(identical(other.adjustingRadius, adjustingRadius) || other.adjustingRadius == adjustingRadius)&&(identical(other.previewRadius, previewRadius) || other.previewRadius == previewRadius)&&(identical(other.previewPoint, previewPoint) || other.previewPoint == previewPoint)&&(identical(other.selectedGeofence, selectedGeofence) || other.selectedGeofence == selectedGeofence)&&(identical(other.editingGeofence, editingGeofence) || other.editingGeofence == editingGeofence)&&(identical(other.selectedFrequentPlace, selectedFrequentPlace) || other.selectedFrequentPlace == selectedFrequentPlace)&&(identical(other.editingFrequentPlace, editingFrequentPlace) || other.editingFrequentPlace == editingFrequentPlace)&&(identical(other.selectedHistoryPosition, selectedHistoryPosition) || other.selectedHistoryPosition == selectedHistoryPosition)&&(identical(other.isFollowing, isFollowing) || other.isFollowing == isFollowing)&&(identical(other.actionsExpanded, actionsExpanded) || other.actionsExpanded == actionsExpanded)&&(identical(other.frequencyExpanded, frequencyExpanded) || other.frequencyExpanded == frequencyExpanded)&&(identical(other.mapZoom, mapZoom) || other.mapZoom == mapZoom)&&(identical(other.historyNavigationIndex, historyNavigationIndex) || other.historyNavigationIndex == historyNavigationIndex)&&(identical(other.historyPlaying, historyPlaying) || other.historyPlaying == historyPlaying)&&(identical(other.isRevealAnimating, isRevealAnimating) || other.isRevealAnimating == isRevealAnimating)&&(identical(other.historyRevealCount, historyRevealCount) || other.historyRevealCount == historyRevealCount)); } @override -int get hashCode => Object.hash(runtimeType,showGeofences,showFrequentPlaces,placingMode,adjustingRadius,previewRadius,previewPoint,selectedGeofence,editingGeofence,selectedFrequentPlace,selectedHistoryPosition,isFollowing,actionsExpanded,frequencyExpanded,mapZoom); +int get hashCode => Object.hashAll([runtimeType,showGeofences,showFrequentPlaces,placingMode,adjustingRadius,previewRadius,previewPoint,selectedGeofence,editingGeofence,selectedFrequentPlace,editingFrequentPlace,selectedHistoryPosition,isFollowing,actionsExpanded,frequencyExpanded,mapZoom,historyNavigationIndex,historyPlaying,isRevealAnimating,historyRevealCount]); @override String toString() { - return 'LocationMapState(showGeofences: $showGeofences, showFrequentPlaces: $showFrequentPlaces, placingMode: $placingMode, adjustingRadius: $adjustingRadius, previewRadius: $previewRadius, previewPoint: $previewPoint, selectedGeofence: $selectedGeofence, editingGeofence: $editingGeofence, selectedFrequentPlace: $selectedFrequentPlace, selectedHistoryPosition: $selectedHistoryPosition, isFollowing: $isFollowing, actionsExpanded: $actionsExpanded, frequencyExpanded: $frequencyExpanded, mapZoom: $mapZoom)'; + return 'LocationMapState(showGeofences: $showGeofences, showFrequentPlaces: $showFrequentPlaces, placingMode: $placingMode, adjustingRadius: $adjustingRadius, previewRadius: $previewRadius, previewPoint: $previewPoint, selectedGeofence: $selectedGeofence, editingGeofence: $editingGeofence, selectedFrequentPlace: $selectedFrequentPlace, editingFrequentPlace: $editingFrequentPlace, selectedHistoryPosition: $selectedHistoryPosition, isFollowing: $isFollowing, actionsExpanded: $actionsExpanded, frequencyExpanded: $frequencyExpanded, mapZoom: $mapZoom, historyNavigationIndex: $historyNavigationIndex, historyPlaying: $historyPlaying, isRevealAnimating: $isRevealAnimating, historyRevealCount: $historyRevealCount)'; } @@ -315,11 +337,11 @@ abstract mixin class _$LocationMapStateCopyWith<$Res> implements $LocationMapSta factory _$LocationMapStateCopyWith(_LocationMapState value, $Res Function(_LocationMapState) _then) = __$LocationMapStateCopyWithImpl; @override @useResult $Res call({ - bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom + bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, FrequentPlaceEntity? editingFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom, int historyNavigationIndex, bool historyPlaying, bool isRevealAnimating, int? historyRevealCount }); -@override $GeofenceEntityCopyWith<$Res>? get selectedGeofence;@override $GeofenceEntityCopyWith<$Res>? get editingGeofence;@override $FrequentPlaceEntityCopyWith<$Res>? get selectedFrequentPlace;@override $PositionEntityCopyWith<$Res>? get selectedHistoryPosition; +@override $GeofenceEntityCopyWith<$Res>? get selectedGeofence;@override $GeofenceEntityCopyWith<$Res>? get editingGeofence;@override $FrequentPlaceEntityCopyWith<$Res>? get selectedFrequentPlace;@override $FrequentPlaceEntityCopyWith<$Res>? get editingFrequentPlace;@override $PositionEntityCopyWith<$Res>? get selectedHistoryPosition; } /// @nodoc @@ -332,7 +354,7 @@ class __$LocationMapStateCopyWithImpl<$Res> /// Create a copy of LocationMapState /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? showGeofences = null,Object? showFrequentPlaces = null,Object? placingMode = null,Object? adjustingRadius = null,Object? previewRadius = null,Object? previewPoint = freezed,Object? selectedGeofence = freezed,Object? editingGeofence = freezed,Object? selectedFrequentPlace = freezed,Object? selectedHistoryPosition = freezed,Object? isFollowing = null,Object? actionsExpanded = null,Object? frequencyExpanded = null,Object? mapZoom = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? showGeofences = null,Object? showFrequentPlaces = null,Object? placingMode = null,Object? adjustingRadius = null,Object? previewRadius = null,Object? previewPoint = freezed,Object? selectedGeofence = freezed,Object? editingGeofence = freezed,Object? selectedFrequentPlace = freezed,Object? editingFrequentPlace = freezed,Object? selectedHistoryPosition = freezed,Object? isFollowing = null,Object? actionsExpanded = null,Object? frequencyExpanded = null,Object? mapZoom = null,Object? historyNavigationIndex = null,Object? historyPlaying = null,Object? isRevealAnimating = null,Object? historyRevealCount = freezed,}) { return _then(_LocationMapState( showGeofences: null == showGeofences ? _self.showGeofences : showGeofences // ignore: cast_nullable_to_non_nullable as bool,showFrequentPlaces: null == showFrequentPlaces ? _self.showFrequentPlaces : showFrequentPlaces // ignore: cast_nullable_to_non_nullable @@ -343,12 +365,17 @@ as double,previewPoint: freezed == previewPoint ? _self.previewPoint : previewPo as LatLng?,selectedGeofence: freezed == selectedGeofence ? _self.selectedGeofence : selectedGeofence // ignore: cast_nullable_to_non_nullable as GeofenceEntity?,editingGeofence: freezed == editingGeofence ? _self.editingGeofence : editingGeofence // ignore: cast_nullable_to_non_nullable as GeofenceEntity?,selectedFrequentPlace: freezed == selectedFrequentPlace ? _self.selectedFrequentPlace : selectedFrequentPlace // ignore: cast_nullable_to_non_nullable +as FrequentPlaceEntity?,editingFrequentPlace: freezed == editingFrequentPlace ? _self.editingFrequentPlace : editingFrequentPlace // ignore: cast_nullable_to_non_nullable as FrequentPlaceEntity?,selectedHistoryPosition: freezed == selectedHistoryPosition ? _self.selectedHistoryPosition : selectedHistoryPosition // ignore: cast_nullable_to_non_nullable as PositionEntity?,isFollowing: null == isFollowing ? _self.isFollowing : isFollowing // ignore: cast_nullable_to_non_nullable as bool,actionsExpanded: null == actionsExpanded ? _self.actionsExpanded : actionsExpanded // ignore: cast_nullable_to_non_nullable as bool,frequencyExpanded: null == frequencyExpanded ? _self.frequencyExpanded : frequencyExpanded // ignore: cast_nullable_to_non_nullable as bool,mapZoom: null == mapZoom ? _self.mapZoom : mapZoom // ignore: cast_nullable_to_non_nullable -as double, +as double,historyNavigationIndex: null == historyNavigationIndex ? _self.historyNavigationIndex : historyNavigationIndex // ignore: cast_nullable_to_non_nullable +as int,historyPlaying: null == historyPlaying ? _self.historyPlaying : historyPlaying // ignore: cast_nullable_to_non_nullable +as bool,isRevealAnimating: null == isRevealAnimating ? _self.isRevealAnimating : isRevealAnimating // ignore: cast_nullable_to_non_nullable +as bool,historyRevealCount: freezed == historyRevealCount ? _self.historyRevealCount : historyRevealCount // ignore: cast_nullable_to_non_nullable +as int?, )); } @@ -392,6 +419,18 @@ $FrequentPlaceEntityCopyWith<$Res>? get selectedFrequentPlace { /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') +$FrequentPlaceEntityCopyWith<$Res>? get editingFrequentPlace { + if (_self.editingFrequentPlace == null) { + return null; + } + + return $FrequentPlaceEntityCopyWith<$Res>(_self.editingFrequentPlace!, (value) { + return _then(_self.copyWith(editingFrequentPlace: value)); + }); +}/// Create a copy of LocationMapState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') $PositionEntityCopyWith<$Res>? get selectedHistoryPosition { if (_self.selectedHistoryPosition == null) { return null; diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_list_sheet.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_list_sheet.dart index 061e9d46..046b477c 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_list_sheet.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_list_sheet.dart @@ -14,8 +14,15 @@ class LocationListSheet extends ConsumerStatefulWidget { final List frequentPlaces; final List positionHistory; final Color primaryColor; + final bool hasHiddenGeofences; + final bool hasHiddenFrequentPlaces; + final bool hasHiddenHistory; final ValueChanged onGeofenceTap; + final ValueChanged? onGeofenceEdit; + final ValueChanged? onGeofenceDelete; final ValueChanged onFrequentPlaceTap; + final ValueChanged? onFrequentPlaceEdit; + final ValueChanged? onFrequentPlaceDelete; final ValueChanged onHistoryTap; const LocationListSheet({ @@ -24,8 +31,15 @@ class LocationListSheet extends ConsumerStatefulWidget { required this.frequentPlaces, required this.positionHistory, required this.primaryColor, + this.hasHiddenGeofences = false, + this.hasHiddenFrequentPlaces = false, + this.hasHiddenHistory = false, required this.onGeofenceTap, + this.onGeofenceEdit, + this.onGeofenceDelete, required this.onFrequentPlaceTap, + this.onFrequentPlaceEdit, + this.onFrequentPlaceDelete, required this.onHistoryTap, }); @@ -34,6 +48,11 @@ class LocationListSheet extends ConsumerStatefulWidget { } class _LocationListSheetState extends ConsumerState { + bool get _hasHiddenItems => + widget.hasHiddenGeofences || + widget.hasHiddenFrequentPlaces || + widget.hasHiddenHistory; + List _filterHistory(String? selectedType) { if (selectedType == null) return widget.positionHistory; return widget.positionHistory @@ -90,6 +109,12 @@ class _LocationListSheetState extends ConsumerState { color: Colors.blue, icon: Icons.shield, onTap: () => widget.onGeofenceTap(g), + onEdit: widget.onGeofenceEdit != null + ? () => widget.onGeofenceEdit!(g) + : null, + onDelete: widget.onGeofenceDelete != null + ? () => widget.onGeofenceDelete!(g) + : null, ), ), const SizedBox(height: 16), @@ -111,6 +136,12 @@ class _LocationListSheetState extends ConsumerState { color: Colors.orange, icon: Icons.home_rounded, onTap: () => widget.onFrequentPlaceTap(fp), + onEdit: widget.onFrequentPlaceEdit != null + ? () => widget.onFrequentPlaceEdit!(fp) + : null, + onDelete: widget.onFrequentPlaceDelete != null + ? () => widget.onFrequentPlaceDelete!(fp) + : null, ), ), const SizedBox(height: 16), @@ -136,11 +167,15 @@ class _LocationListSheetState extends ConsumerState { padding: const EdgeInsets.only(top: 40), child: Center( child: Text( - context.translate(I18n.locationListNoItems), + _hasHiddenItems + ? context.translate( + I18n.locationListHiddenItems) + : context.translate(I18n.locationListNoItems), style: const TextStyle( color: Colors.grey, fontSize: 14, ), + textAlign: TextAlign.center, ), ), ), @@ -273,6 +308,8 @@ class _LocationListSheetState extends ConsumerState { required Color color, required IconData icon, required VoidCallback onTap, + VoidCallback? onEdit, + VoidCallback? onDelete, }) { return Card( margin: const EdgeInsets.only(bottom: 6), @@ -302,11 +339,41 @@ class _LocationListSheetState extends ConsumerState { subtitle, style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant), ), - trailing: Icon( - Icons.chevron_right, - color: Theme.of(context).colorScheme.outline, - size: 20, - ), + trailing: onEdit != null || onDelete != null + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (onEdit != null) + GestureDetector( + onTap: onEdit, + child: Padding( + padding: const EdgeInsets.all(6), + child: Icon( + Icons.edit_outlined, + size: 18, + color: color, + ), + ), + ), + if (onDelete != null) + GestureDetector( + onTap: onDelete, + child: Padding( + padding: const EdgeInsets.all(6), + child: Icon( + Icons.delete_outline, + size: 18, + color: Theme.of(context).colorScheme.error, + ), + ), + ), + ], + ) + : Icon( + Icons.chevron_right, + color: Theme.of(context).colorScheme.outline, + size: 20, + ), onTap: onTap, ), ); diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_map.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_map.dart index f0331119..1128d4ff 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_map.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_map.dart @@ -20,11 +20,15 @@ import 'package:utils/utils.dart'; import 'device_banner.dart'; import 'name_input_sheet.dart'; import 'location_list_sheet.dart'; -import 'map_controls/layer_toggles.dart'; +import 'map_controls/gps_control_panel.dart'; +import 'map_controls/history_player.dart'; +import 'map_controls/map_action_button.dart'; +import 'map_controls/gps_functions_dialog.dart'; import 'map_controls/map_actions_panel.dart'; import 'map_controls/map_style_selector.dart'; import 'map_controls/placement_banner.dart'; import 'map_controls/radius_slider_bar.dart'; +import 'map_controls/reveal_progress_bar.dart'; import 'map_info_cards/frequent_place_info_card.dart'; import 'map_info_cards/geofence_info_card.dart'; import 'map_info_cards/history_position_info_card.dart'; @@ -77,14 +81,17 @@ class _LocationMapState extends ConsumerState LocationMapController get _vm => ref.read(locationMapControllerProvider.notifier); - Color get _primaryColor => - context.sfColors.legacyPrimary; + Color get _primaryColor => context.sfColors.legacyPrimary; @override void initState() { super.initState(); _mapController = MapController(); _startMonitoring(); + _vm.onPlaybackTick = () { + final state = ref.read(locationMapControllerProvider); + _navigateToHistoryPosition(state.historyNavigationIndex); + }; } static const _minFrequency = 60; @@ -94,7 +101,9 @@ class _LocationMapState extends ConsumerState final raw = widget.selectedDevice?.settings.frequency ?? 60; final frequency = raw < _minFrequency ? _minFrequency : raw; _followTimer = Timer.periodic(Duration(seconds: frequency), (_) { - if (ref.read(selectedDeviceProvider).value?.isDisconnected ?? true) return; + if (ref.read(selectedDeviceProvider).value?.isDisconnected ?? true) { + return; + } widget.onRefreshPosition(); }); } @@ -102,6 +111,9 @@ class _LocationMapState extends ConsumerState @override void dispose() { _followTimer?.cancel(); + _vm.onPlaybackTick = null; + _vm.stopPlaybackTimer(); + _vm.cancelRecenter(); _moveAnimation?.dispose(); _mapController.dispose(); super.dispose(); @@ -131,12 +143,80 @@ class _LocationMapState extends ConsumerState (oldWidget.positionHistory.length != widget.positionHistory.length || !oldWidget.showRouteTrail)) { _fitHistoryBounds(); + if (oldWidget.positionHistory.length != widget.positionHistory.length) { + Future(() { + _vm.startHistoryNavigation(); + _startRevealAnimation(); + }); + } } } - void _animatedMove(LatLng dest, double zoom) { + void _animatedMove( + LatLng dest, + double? zoom, { + Duration duration = const Duration(milliseconds: 400), + Curve curve = Curves.easeInOut, + }) { + _animatedMoveAsync(dest, zoom, duration: duration, curve: curve); + } + + Future _startRevealAnimation() async { + final positions = widget.positionHistory + .where((p) => p.latitude != 0 || p.longitude != 0) + .toList(); + if (positions.length <= 1) return; + + _vm.startReveal(); + + await _animatedMoveAsync( + LatLng(positions.first.latitude, positions.first.longitude), + 18.0, + duration: const Duration(milliseconds: 1000), + curve: Curves.easeOutCubic, + ); + + for (var i = 1; i < positions.length; i++) { + if (!mounted) return; + final state = ref.read(locationMapControllerProvider); + if (!state.isRevealAnimating) return; + _vm.updateRevealCount(i + 1); + await _animatedMoveAsync( + LatLng(positions[i].latitude, positions[i].longitude), + null, + duration: const Duration(milliseconds: 800), + curve: Curves.easeInOutCubic, + ); + if (!mounted) return; + if (!ref.read(locationMapControllerProvider).isRevealAnimating) return; + await Future.delayed(const Duration(milliseconds: 200)); + } + + if (!mounted) return; + _vm.finishReveal(); + await Future.delayed(const Duration(milliseconds: 500)); + if (!mounted) return; + _fitHistoryBounds(); + } + + void _skipRevealAnimation() { + _moveAnimation?.dispose(); + _moveAnimation = null; + _vm.finishReveal(); + Future(_vm.startHistoryNavigation); + _fitHistoryBounds(); + } + + Future _animatedMoveAsync( + LatLng dest, + double? zoom, { + Duration duration = const Duration(milliseconds: 400), + Curve curve = Curves.easeInOut, + }) { + final completer = Completer(); _moveAnimation?.dispose(); final camera = _mapController.camera; + final targetZoom = zoom ?? camera.zoom; final latTween = Tween( begin: camera.center.latitude, end: dest.latitude, @@ -145,18 +225,12 @@ class _LocationMapState extends ConsumerState begin: camera.center.longitude, end: dest.longitude, ); - final zoomTween = Tween(begin: camera.zoom, end: zoom); + final zoomTween = Tween(begin: camera.zoom, end: targetZoom); - final controller = AnimationController( - duration: const Duration(milliseconds: 400), - vsync: this, - ); + final controller = AnimationController(duration: duration, vsync: this); _moveAnimation = controller; - final animation = CurvedAnimation( - parent: controller, - curve: Curves.easeInOut, - ); + final animation = CurvedAnimation(parent: controller, curve: curve); controller.addListener(() { _mapController.move( @@ -170,10 +244,53 @@ class _LocationMapState extends ConsumerState status == AnimationStatus.dismissed) { controller.dispose(); if (_moveAnimation == controller) _moveAnimation = null; + if (!completer.isCompleted) completer.complete(); } }); controller.forward(); + return completer.future; + } + + bool _isHistoryNavigating(LocationMapState mapState) => + widget.showRouteTrail && + widget.positionHistory.isNotEmpty && + mapState.historyNavigationIndex >= 0 && + mapState.historyNavigationIndex < widget.positionHistory.length; + + void _navigateToHistoryPosition(int index) { + if (index < 0 || index >= widget.positionHistory.length) return; + final position = widget.positionHistory[index]; + if (position.latitude == 0 && position.longitude == 0) return; + _animatedMove(LatLng(position.latitude, position.longitude), null); + } + + Future _handleToggleFollow(LocationMapState mapState) async { + final willActivate = !mapState.isFollowing; + if (willActivate && !await guardDeviceCommand(context, ref)) return; + if (!mounted) return; + _vm.toggleFollowing(); + unawaited( + ref + .read(sfTrackingProvider) + .legacyLocationMapFollowToggled(willActivate), + ); + if (willActivate && widget.selectedPosition != null) { + _centerOnDevice(); + } + } + + void _togglePlayback() { + _vm.toggleHistoryPlayback(widget.positionHistory.length); + final state = ref.read(locationMapControllerProvider); + if (state.historyPlaying) { + _navigateToHistoryPosition(state.historyNavigationIndex); + } + } + + void _scheduleRecenter() { + if (widget.selectedPosition == null) return; + _vm.scheduleRecenter(_centerOnDevice); } void _centerOnDevice() { @@ -245,6 +362,46 @@ class _LocationMapState extends ConsumerState ); } + Future _showGpsFunctionsDialog( + BuildContext context, + WidgetRef ref, + LocationMapState mapState, + ) async { + final result = await showGpsFunctionsDialog( + context, + isFollowing: mapState.isFollowing, + showGeofences: mapState.showGeofences, + showFrequentPlaces: mapState.showFrequentPlaces, + showRouteTrail: widget.showRouteTrail, + hasPositionHistory: widget.positionHistory.isNotEmpty, + hasPosition: widget.selectedPosition != null, + ); + if (result == null || !mounted) return; + + switch (result) { + case GpsFunction.list: + _showListSheet(); + case GpsFunction.addGeofence: + _vm.startPlacing(PlacingMode.geofence); + case GpsFunction.addFrequentPlace: + _vm.startPlacing(PlacingMode.frequentPlace); + case GpsFunction.share: + _shareLocation(); + case GpsFunction.centerOnDevice: + _centerOnDevice(); + case GpsFunction.toggleFollow: + _handleToggleFollow(mapState); + case GpsFunction.toggleGeofences: + _vm.toggleGeofences(); + case GpsFunction.toggleFrequentPlaces: + _vm.toggleFrequentPlaces(); + case GpsFunction.positionHistory: + _handleHistoryTap(); + case GpsFunction.toggleRouteTrail: + ref.read(locationControllerProvider.notifier).toggleRouteTrail(); + } + } + Future _updateFrequency(int frequency) async { if (!await guardDeviceCommand(context, ref)) return; final success = await ref @@ -253,7 +410,9 @@ class _LocationMapState extends ConsumerState if (!mounted) return; if (success) { _followTimer?.cancel(); - final safeFrequency = frequency < _minFrequency ? _minFrequency : frequency; + final safeFrequency = frequency < _minFrequency + ? _minFrequency + : frequency; _followTimer = Timer.periodic(Duration(seconds: safeFrequency), (_) { widget.onRefreshPosition(); }); @@ -274,19 +433,41 @@ class _LocationMapState extends ConsumerState if (mapState.placingMode == PlacingMode.geofence) { _vm.confirmGeofencePlacement(center); } else { + final editing = mapState.editingFrequentPlace; _vm.confirmFrequentPlacePlacement(); showNameInputSheet( context, ref: ref, - title: context.translate(I18n.locationNewFrequentPlace), + title: context.translate( + editing != null + ? I18n.locationEditFrequentPlace + : I18n.locationNewFrequentPlace, + ), hintText: context.translate(I18n.locationHintFrequentPlace), - onSubmit: (name, _) => ref - .read(locationControllerProvider.notifier) - .createFrequentPlace( - name: name, - lat: center.latitude, - lng: center.longitude, - ), + initialName: editing?.name, + submitLabel: editing != null + ? context.translate(I18n.locationSave) + : null, + onSubmit: (name, _) { + if (editing != null) { + return ref + .read(locationControllerProvider.notifier) + .updateFrequentPlace( + id: editing.id, + name: name, + lat: center.latitude, + lng: center.longitude, + wifiList: editing.wifiList, + ); + } + return ref + .read(locationControllerProvider.notifier) + .createFrequentPlace( + name: name, + lat: center.latitude, + lng: center.longitude, + ); + }, ); } } @@ -356,15 +537,57 @@ class _LocationMapState extends ConsumerState positionHistory: locationState.showRouteTrail ? locationState.positionHistory : [], + hasHiddenGeofences: + !mapState.showGeofences && locationState.geofences.isNotEmpty, + hasHiddenFrequentPlaces: + !mapState.showFrequentPlaces && + locationState.frequentPlaces.isNotEmpty, + hasHiddenHistory: + !locationState.showRouteTrail && + locationState.positionHistory.isNotEmpty, primaryColor: _primaryColor, onGeofenceTap: (g) { Navigator.pop(context); _animatedMove(LatLng(g.latitude, g.longitude), _defaultZoom); }, + onGeofenceEdit: (g) { + Navigator.pop(context); + _vm.selectGeofence(g); + }, + onGeofenceDelete: (g) async { + Navigator.pop(context); + final confirmed = await _confirmDelete( + context, + I18n.locationDeleteGeofenceConfirm, + ); + if (!confirmed || !mounted) return; + if (!await guardDeviceCommand(context, ref)) return; + if (!mounted) return; + ref + .read(locationControllerProvider.notifier) + .deleteGeofence(id: g.id); + }, onFrequentPlaceTap: (fp) { Navigator.pop(context); _animatedMove(LatLng(fp.lat, fp.lng), _defaultZoom); }, + onFrequentPlaceEdit: (fp) { + Navigator.pop(context); + _onEditFrequentPlace(fp); + }, + onFrequentPlaceDelete: (fp) async { + Navigator.pop(context); + final confirmed = await _confirmDelete( + context, + I18n.locationDeleteFrequentPlaceConfirm, + ); + if (!confirmed || !mounted) return; + if (!await guardDeviceCommand(context, ref)) return; + if (!mounted) return; + ref + .read(locationControllerProvider.notifier) + .deleteFrequentPlace(id: fp.id); + }, onHistoryTap: (p) { Navigator.pop(context); if (p.latitude != 0 || p.longitude != 0) { @@ -375,12 +598,34 @@ class _LocationMapState extends ConsumerState ); } + Future _confirmDelete(BuildContext context, String messageKey) async { + final result = await showLegacyDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(context.translate(I18n.delete)), + content: Text(context.translate(messageKey)), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(false), + child: Text(context.translate(I18n.cancel)), + ), + FilledButton( + onPressed: () => Navigator.of(dialogContext).pop(true), + style: FilledButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error, + ), + child: Text(context.translate(I18n.delete)), + ), + ], + ), + ); + return result == true; + } + void _handleHistoryTap() { - if (widget.positionHistory.isEmpty) { - _openDateRangePicker(); - } else { - ref.read(locationControllerProvider.notifier).toggleRouteTrail(); - } + _vm.stopPlaybackTimer(); + _vm.stopHistoryNavigation(); + _openDateRangePicker(); } Future _openDateRangePicker() async { @@ -394,12 +639,7 @@ class _LocationMapState extends ConsumerState start: now.subtract(const Duration(days: 1)), end: now, ), - builder: (context, child) => Theme( - data: Theme.of( - context, - ).copyWith(colorScheme: ColorScheme.light(primary: _primaryColor)), - child: child!, - ), + builder: legacyPickerThemeBuilder, ); if (picked != null) { @@ -411,33 +651,31 @@ class _LocationMapState extends ConsumerState 59, 59, ); - ref + if (!mounted) return; + showInfoDialog(context, I18n.locationHistoryLoading); + await ref .read(locationControllerProvider.notifier) .loadPositionHistory(from: picked.start, to: to); + if (!mounted) return; + final history = + ref.read(locationControllerProvider).value?.positionHistory ?? []; + if (history.isEmpty) { + await showErrorDialog(context, I18n.locationHistoryEmpty); + } else { + await showSuccessDialog( + context, + I18n.locationHistoryLoaded, + args: {'count': history.length}, + ); + } } } Future _onEditFrequentPlace(FrequentPlaceEntity fp) async { if (!await guardDeviceCommand(context, ref)) return; if (!mounted) return; - _vm.clearSelectedFrequentPlace(); - showNameInputSheet( - context, - ref: ref, - title: context.translate(I18n.locationEditFrequentPlace), - hintText: context.translate(I18n.locationHintFrequentPlace), - initialName: fp.name, - submitLabel: context.translate(I18n.locationSave), - onSubmit: (name, _) => ref - .read(locationControllerProvider.notifier) - .updateFrequentPlace( - id: fp.id, - name: name, - lat: fp.lat, - lng: fp.lng, - wifiList: fp.wifiList, - ), - ); + _vm.startEditingFrequentPlace(fp); + _animatedMove(LatLng(fp.lat, fp.lng), null); } List _buildMapLayers(LocationMapState mapState) { @@ -448,6 +686,8 @@ class _LocationMapState extends ConsumerState zoom: mapState.mapZoom, onPositionTap: _vm.selectHistoryPosition, onClusterTap: _animatedMove, + activeIndex: mapState.historyNavigationIndex, + visibleCount: mapState.historyRevealCount, ) : null; @@ -488,17 +728,68 @@ class _LocationMapState extends ConsumerState ], ), MarkerLayer(markers: _buildMarkers(mapState, historyLayer)), - if (widget.selectedDevice != null) - Align( - alignment: Alignment.bottomCenter, - child: DeviceBanner( - device: widget.selectedDevice!, - devices: widget.devices, - positions: widget.positions, - onDeviceChanged: widget.onDeviceChanged, - onTap: _centerOnDevice, + if (mapState.isRevealAnimating) ...[ + Positioned( + bottom: SizeUtils.getByScreen(small: 16, big: 14), + left: 0, + right: 0, + child: RevealProgressBar( + current: mapState.historyRevealCount ?? 0, + total: widget.positionHistory.length, + primaryColor: _primaryColor, + onSkip: _skipRevealAnimation, ), ), + ] else if (_isHistoryNavigating(mapState)) ...[ + Positioned( + bottom: SizeUtils.getByScreen(small: 16, big: 14), + left: 0, + right: 0, + child: HistoryPlayer( + currentIndex: mapState.historyNavigationIndex, + total: widget.positionHistory.length, + isPlaying: mapState.historyPlaying, + currentPosition: + widget.positionHistory[mapState.historyNavigationIndex], + onPrevious: () { + _vm.previousHistoryPosition(); + final state = ref.read(locationMapControllerProvider); + _navigateToHistoryPosition(state.historyNavigationIndex); + }, + onNext: () { + _vm.nextHistoryPosition(widget.positionHistory.length); + final state = ref.read(locationMapControllerProvider); + _navigateToHistoryPosition(state.historyNavigationIndex); + }, + onPlayPause: _togglePlayback, + onClose: () { + _vm.stopPlaybackTimer(); + _vm.stopHistoryNavigation(); + }, + ), + ), + ] else ...[ + if (widget.selectedPosition != null) + Positioned( + bottom: SizeUtils.getByScreen(small: 190, big: 186), + right: 16, + child: MapActionButton( + icon: Icons.my_location, + onTap: _centerOnDevice, + ), + ), + if (widget.selectedDevice != null) + Align( + alignment: Alignment.bottomCenter, + child: DeviceBanner( + device: widget.selectedDevice!, + devices: widget.devices, + positions: widget.positions, + onDeviceChanged: widget.onDeviceChanged, + onTap: _centerOnDevice, + ), + ), + ], ]; } @@ -538,7 +829,7 @@ class _LocationMapState extends ConsumerState width: 40, height: 40, child: GestureDetector( - onLongPress: () => _vm.selectFrequentPlace(fp), + onTap: () => _vm.selectFrequentPlace(fp), child: Container( decoration: BoxDecoration( color: Colors.orange.withValues(alpha: 0.3), @@ -567,7 +858,7 @@ class _LocationMapState extends ConsumerState width: 36, height: 36, child: GestureDetector( - onLongPress: () => _vm.selectGeofence(g), + onTap: () => _vm.selectGeofence(g), child: Container( decoration: BoxDecoration( color: Colors.blue.withValues(alpha: 0.3), @@ -643,7 +934,12 @@ class _LocationMapState extends ConsumerState const SizedBox(height: 8), FrequencySelector( currentFrequency: - ref.watch(selectedDeviceProvider).value?.settings.frequency ?? 60, + ref + .watch(selectedDeviceProvider) + .value + ?.settings + .frequency ?? + 60, options: widget.selectedDevice!.capabilities!.location!.options .where((o) => o > 0 || o == -1) .toList(), @@ -656,31 +952,8 @@ class _LocationMapState extends ConsumerState Positioned( top: 12, right: 12, - child: LayerToggles( - showGeofences: mapState.showGeofences, - showFrequentPlaces: mapState.showFrequentPlaces, - showRouteTrail: widget.showRouteTrail, - hasPositionHistory: widget.positionHistory.isNotEmpty, - isLoadingHistory: widget.isLoadingHistory, - onGeofencesToggled: _vm.toggleGeofences, - onFrequentPlacesToggled: _vm.toggleFrequentPlaces, - onHistoryTapped: _handleHistoryTap, - onHistoryLongPressed: _openDateRangePicker, - ), - ), - Positioned( - bottom: SizeUtils.getByScreen(small: 120, big: 110), - right: 12, - child: MapActionsPanel( - actionsExpanded: mapState.actionsExpanded, - hasPosition: widget.selectedPosition != null, - isFollowing: mapState.isFollowing, - onToggleExpanded: _vm.toggleActionsExpanded, - onListTap: _showListSheet, - onAddGeofence: () => _vm.startPlacing(PlacingMode.geofence), - onAddFrequentPlace: () => _vm.startPlacing(PlacingMode.frequentPlace), - onShareTap: _shareLocation, - onRefreshTap: () async { + child: GpsControlPanel( + onRefresh: () async { if (!await guardDeviceCommand(context, ref)) return; unawaited( ref.read(sfTrackingProvider).legacyLocationMapRefreshTapped(), @@ -694,27 +967,7 @@ class _LocationMapState extends ConsumerState await showErrorDialog(context, I18n.errorPositions); } }, - onCenterTap: _centerOnDevice, - onToggleFollow: () async { - final willActivate = !mapState.isFollowing; - if (willActivate && !await guardDeviceCommand(context, ref)) return; - _vm.toggleFollowing(); - unawaited( - ref - .read(sfTrackingProvider) - .legacyLocationMapFollowToggled(willActivate), - ); - if (willActivate && widget.selectedPosition != null) { - _centerOnDevice(); - } - if (!mounted) return; - await showSuccessDialog( - context, - willActivate - ? I18n.locationMapFollowEnabled - : I18n.locationMapFollowDisabled, - ); - }, + onSettings: () => _showGpsFunctionsDialog(context, ref, mapState), ), ), ]; @@ -791,10 +1044,13 @@ class _LocationMapState extends ConsumerState initialZoom: initialZoom, minZoom: 5, keepAlive: true, - onPositionChanged: (camera, _) { + onPositionChanged: (camera, hasGesture) { if (widget.showRouteTrail && widget.positionHistory.length > 2) { _vm.updateMapZoom(camera.zoom); } + if (hasGesture) { + _scheduleRecenter(); + } }, ), children: _buildMapLayers(mapState), diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/gps_control_panel.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/gps_control_panel.dart new file mode 100644 index 00000000..c055e4d4 --- /dev/null +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/gps_control_panel.dart @@ -0,0 +1,77 @@ +import 'package:legacy_theme/legacy_theme.dart'; +import 'package:flutter/material.dart'; + +class GpsControlPanel extends StatelessWidget { + final VoidCallback onRefresh; + final VoidCallback onSettings; + + const GpsControlPanel({ + super.key, + required this.onRefresh, + required this.onSettings, + }); + + @override + Widget build(BuildContext context) { + final primaryColor = context.sfColors.legacyPrimary; + + return Container( + decoration: BoxDecoration( + color: primaryColor, + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.15), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + padding: const EdgeInsets.all(6), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _PanelButton( + icon: Icons.refresh, + onTap: onRefresh, + ), + const SizedBox(height: 4), + _PanelButton( + icon: Icons.settings, + onTap: onSettings, + ), + ], + ), + ); + } +} + +class _PanelButton extends StatelessWidget { + final IconData icon; + final VoidCallback onTap; + + const _PanelButton({ + required this.icon, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + icon, + color: Colors.white, + size: 24, + ), + ), + ); + } +} diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/gps_functions_dialog.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/gps_functions_dialog.dart new file mode 100644 index 00000000..79cd760c --- /dev/null +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/gps_functions_dialog.dart @@ -0,0 +1,179 @@ +import 'package:legacy_theme/legacy_theme.dart'; +import 'package:legacy_ui/legacy_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:sf_localizations/sf_localizations.dart'; + +enum GpsFunction { + list, + addGeofence, + addFrequentPlace, + share, + centerOnDevice, + toggleFollow, + toggleGeofences, + toggleFrequentPlaces, + positionHistory, + toggleRouteTrail, +} + +Future showGpsFunctionsDialog( + BuildContext context, { + required bool isFollowing, + required bool showGeofences, + required bool showFrequentPlaces, + required bool showRouteTrail, + required bool hasPositionHistory, + required bool hasPosition, +}) { + return showLegacyDialog( + context: context, + builder: (dialogContext) => _GpsFunctionsDialog( + isFollowing: isFollowing, + showGeofences: showGeofences, + showFrequentPlaces: showFrequentPlaces, + showRouteTrail: showRouteTrail, + hasPositionHistory: hasPositionHistory, + hasPosition: hasPosition, + ), + ); +} + +class _GpsFunctionsDialog extends StatelessWidget { + final bool isFollowing; + final bool showGeofences; + final bool showFrequentPlaces; + final bool showRouteTrail; + final bool hasPositionHistory; + final bool hasPosition; + + const _GpsFunctionsDialog({ + required this.isFollowing, + required this.showGeofences, + required this.showFrequentPlaces, + required this.showRouteTrail, + required this.hasPositionHistory, + required this.hasPosition, + }); + + @override + Widget build(BuildContext context) { + final primaryColor = context.sfColors.legacyPrimary; + + return AlertDialog( + title: Center( + child: Text( + context.translate(I18n.gpsFunctionsTitle), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: primaryColor, + ), + ), + ), + contentPadding: const EdgeInsets.only(top: 12, bottom: 8), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _FunctionTile( + icon: Icons.list_alt, + label: context.translate(I18n.gpsFunctionList), + onTap: () => Navigator.pop(context, GpsFunction.list), + ), + _FunctionTile( + icon: Icons.add_location_alt, + label: context.translate(I18n.gpsFunctionAddGeofence), + onTap: () => Navigator.pop(context, GpsFunction.addGeofence), + visibilityIcon: showGeofences, + onVisibilityTap: () => + Navigator.pop(context, GpsFunction.toggleGeofences), + ), + _FunctionTile( + icon: Icons.add_home, + label: context.translate(I18n.gpsFunctionAddFrequentPlace), + onTap: () => + Navigator.pop(context, GpsFunction.addFrequentPlace), + visibilityIcon: showFrequentPlaces, + onVisibilityTap: () => + Navigator.pop(context, GpsFunction.toggleFrequentPlaces), + ), + if (hasPosition) + _FunctionTile( + icon: Icons.share, + label: context.translate(I18n.gpsFunctionShare), + onTap: () => Navigator.pop(context, GpsFunction.share), + ), + if (hasPosition) + _FunctionTile( + icon: isFollowing ? Icons.gps_fixed : Icons.gps_not_fixed, + label: context.translate( + isFollowing + ? I18n.gpsFunctionStopFollow + : I18n.gpsFunctionFollow, + ), + onTap: () => Navigator.pop(context, GpsFunction.toggleFollow), + ), + _FunctionTile( + icon: Icons.route, + label: context.translate(I18n.locationLayerHistory), + onTap: () => + Navigator.pop(context, GpsFunction.positionHistory), + visibilityIcon: + hasPositionHistory ? showRouteTrail : null, + onVisibilityTap: hasPositionHistory + ? () => + Navigator.pop(context, GpsFunction.toggleRouteTrail) + : null, + ), + ], + ), + ), + ); + } +} + +class _FunctionTile extends StatelessWidget { + final IconData icon; + final String label; + final VoidCallback onTap; + final bool? visibilityIcon; + final VoidCallback? onVisibilityTap; + + const _FunctionTile({ + required this.icon, + required this.label, + required this.onTap, + this.visibilityIcon, + this.onVisibilityTap, + }); + + @override + Widget build(BuildContext context) { + final primaryColor = context.sfColors.legacyPrimary; + + return ListTile( + dense: true, + leading: Icon(icon, color: primaryColor, size: 22), + title: Text( + label, + style: const TextStyle(fontSize: 14), + ), + trailing: visibilityIcon != null && onVisibilityTap != null + ? GestureDetector( + onTap: onVisibilityTap, + child: Padding( + padding: const EdgeInsets.all(4), + child: Icon( + visibilityIcon! ? Icons.visibility : Icons.visibility_off, + size: 22, + color: visibilityIcon! + ? primaryColor + : Theme.of(context).colorScheme.outline, + ), + ), + ) + : const Icon(Icons.chevron_right, size: 20), + onTap: onTap, + ); + } +} diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/history_player.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/history_player.dart new file mode 100644 index 00000000..978acbf6 --- /dev/null +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/history_player.dart @@ -0,0 +1,199 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:legacy_device_state/legacy_device_state.dart'; +import 'package:legacy_theme/legacy_theme.dart'; + +const _playbackIntervalSeconds = 3; + +class HistoryPlayer extends StatelessWidget { + final int currentIndex; + final int total; + final bool isPlaying; + final PositionEntity currentPosition; + final VoidCallback onPrevious; + final VoidCallback onNext; + final VoidCallback onPlayPause; + final VoidCallback onClose; + + const HistoryPlayer({ + super.key, + required this.currentIndex, + required this.total, + required this.isPlaying, + required this.currentPosition, + required this.onPrevious, + required this.onNext, + required this.onPlayPause, + required this.onClose, + }); + + @override + Widget build(BuildContext context) { + final primaryColor = context.sfColors.legacyPrimary; + final date = DateTime.fromMillisecondsSinceEpoch( + currentPosition.positionDate, + ); + final dateText = DateFormat('dd/MM/yyyy HH:mm').format(date); + final addressParts = [ + currentPosition.address?.street, + currentPosition.address?.city, + ].where((s) => s != null && s.isNotEmpty && s != 'Unknown').join(', '); + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.15), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Icon(Icons.location_on, color: primaryColor, size: 18), + const SizedBox(width: 6), + Expanded( + child: Text( + addressParts.isNotEmpty + ? addressParts + : currentPosition.type, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + GestureDetector( + onTap: onClose, + child: Padding( + padding: const EdgeInsets.all(4), + child: Icon( + Icons.close, + size: 20, + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + ], + ), + const SizedBox(height: 2), + Row( + children: [ + const SizedBox(width: 24), + Text( + '$dateText ยท ${currentPosition.type}', + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: currentIndex > 0 ? onPrevious : null, + icon: const Icon(Icons.skip_previous), + color: primaryColor, + iconSize: 28, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 40, minHeight: 40), + ), + const SizedBox(width: 8), + GestureDetector( + onTap: onPlayPause, + child: Stack( + alignment: Alignment.center, + children: [ + if (isPlaying) + _CountdownRing( + key: ValueKey(currentIndex), + primaryColor: primaryColor, + ), + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: primaryColor, + shape: BoxShape.circle, + ), + child: Icon( + isPlaying ? Icons.pause : Icons.play_arrow, + color: Colors.white, + size: 26, + ), + ), + ], + ), + ), + const SizedBox(width: 8), + IconButton( + onPressed: currentIndex < total - 1 ? onNext : null, + icon: const Icon(Icons.skip_next), + color: primaryColor, + iconSize: 28, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 40, minHeight: 40), + ), + ], + ), + const SizedBox(height: 4), + LinearProgressIndicator( + value: total > 1 ? currentIndex / (total - 1) : 0, + backgroundColor: + Theme.of(context).colorScheme.outline.withValues(alpha: 0.2), + valueColor: AlwaysStoppedAnimation(primaryColor), + borderRadius: BorderRadius.circular(2), + minHeight: 3, + ), + const SizedBox(height: 4), + Text( + '${currentIndex + 1} / $total', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ); + } +} + +class _CountdownRing extends StatelessWidget { + final Color primaryColor; + + const _CountdownRing({super.key, required this.primaryColor}); + + @override + Widget build(BuildContext context) { + return TweenAnimationBuilder( + tween: Tween(begin: 1.0, end: 0.0), + duration: const Duration(seconds: _playbackIntervalSeconds), + builder: (context, value, _) => SizedBox( + width: 48, + height: 48, + child: CircularProgressIndicator( + value: value, + strokeWidth: 2.5, + valueColor: AlwaysStoppedAnimation(primaryColor), + backgroundColor: primaryColor.withValues(alpha: 0.2), + ), + ), + ); + } +} diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/map_actions_panel.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/map_actions_panel.dart index 01491eee..128c5be1 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/map_actions_panel.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/map_actions_panel.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:legacy_theme/legacy_theme.dart'; import 'package:location/src/features/location/presentation/providers/location_map_controller.dart'; import 'package:sf_localizations/sf_localizations.dart'; import 'package:utils/utils.dart'; @@ -120,6 +121,7 @@ class FrequencySelector extends ConsumerWidget { } Widget _buildSegmented(BuildContext context, LocationMapController vm) { + final primaryColor = context.sfColors.legacyPrimary; return Material( elevation: 2, borderRadius: BorderRadius.circular(8), @@ -142,9 +144,7 @@ class FrequencySelector extends ConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 6), margin: const EdgeInsets.symmetric(vertical: 2), decoration: BoxDecoration( - color: selected - ? Theme.of(context).primaryColor - : Colors.transparent, + color: selected ? primaryColor : Colors.transparent, borderRadius: BorderRadius.circular(6), ), child: Center( @@ -156,7 +156,7 @@ class FrequencySelector extends ConsumerWidget { fontSize: opt == -1 ? 10 : 12, fontWeight: FontWeight.w600, color: selected - ? Theme.of(context).colorScheme.surface + ? Colors.white : Theme.of(context).colorScheme.onSurface, ), ), @@ -167,10 +167,10 @@ class FrequencySelector extends ConsumerWidget { const SizedBox(height: 2), GestureDetector( onTap: vm.collapseFrequency, - child: const SizedBox( + child: SizedBox( width: 40, height: 24, - child: Icon(Icons.close, size: 16, color: Colors.black54), + child: Icon(Icons.close, size: 16, color: primaryColor), ), ), ], diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/map_style_selector.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/map_style_selector.dart index 9e8b3cc7..dcb0cd88 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/map_style_selector.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/map_style_selector.dart @@ -34,10 +34,10 @@ class MapStyleSelector extends ConsumerWidget { onTap: () => ref.read(mapStyleSelectorExpandedProvider.notifier).set(true), borderRadius: BorderRadius.circular(8), - child: const SizedBox( + child: SizedBox( width: 40, height: 40, - child: Icon(Icons.layers, size: 22, color: Colors.grey), + child: Icon(Icons.layers, size: 22, color: primaryColor), ), ), ); diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/reveal_progress_bar.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/reveal_progress_bar.dart new file mode 100644 index 00000000..e1fe3e7d --- /dev/null +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/map_controls/reveal_progress_bar.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:sf_localizations/sf_localizations.dart'; + +class RevealProgressBar extends StatelessWidget { + final int current; + final int total; + final Color primaryColor; + final VoidCallback onSkip; + + const RevealProgressBar({ + super.key, + required this.current, + required this.total, + required this.primaryColor, + required this.onSkip, + }); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.12), + blurRadius: 6, + offset: const Offset(0, 3), + ), + ], + ), + child: Row( + children: [ + Icon(Icons.route, color: primaryColor, size: 20), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.translate(I18n.locationRevealProgress), + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 4), + LinearProgressIndicator( + value: total > 1 ? current / total : 0, + backgroundColor: Theme.of(context) + .colorScheme + .outline + .withValues(alpha: 0.2), + valueColor: AlwaysStoppedAnimation(primaryColor), + borderRadius: BorderRadius.circular(2), + minHeight: 3, + ), + const SizedBox(height: 2), + Text( + '$current / $total', + style: TextStyle( + fontSize: 10, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + const SizedBox(width: 10), + GestureDetector( + onTap: onSkip, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + context.translate(I18n.locationRevealSkip), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: primaryColor, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/route_history_layer.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/route_history_layer.dart index 91e92fd8..9c36850b 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/route_history_layer.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/route_history_layer.dart @@ -21,6 +21,8 @@ class RouteHistoryLayer extends StatelessWidget { final double zoom; final ValueChanged onPositionTap; final void Function(LatLng point, double zoom) onClusterTap; + final int activeIndex; + final int? visibleCount; const RouteHistoryLayer({ super.key, @@ -28,19 +30,56 @@ class RouteHistoryLayer extends StatelessWidget { required this.zoom, required this.onPositionTap, required this.onClusterTap, + this.activeIndex = -1, + this.visibleCount, }); + List get _visiblePositions { + if (visibleCount == null) return positionHistory; + return positionHistory.take(visibleCount!).toList(); + } + @override Widget build(BuildContext context) { return MarkerLayer( - markers: [..._buildDirectionArrows(), ..._buildHistoryMarkers()], + markers: [ + ..._buildDirectionArrows(), + ..._buildHistoryMarkers(), + if (activeIndex >= 0 && activeIndex < positionHistory.length) + _buildActiveMarker(positionHistory[activeIndex]), + ], + ); + } + + Marker _buildActiveMarker(PositionEntity position) { + return Marker( + point: LatLng(position.latitude, position.longitude), + width: 40, + height: 40, + child: Container( + decoration: BoxDecoration( + color: routeArrowColor, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 3), + boxShadow: [ + BoxShadow( + color: routeArrowColor.withValues(alpha: 0.5), + blurRadius: 8, + spreadRadius: 2, + ), + ], + ), + child: const Icon(Icons.navigation, color: Colors.white, size: 20), + ), + rotate: true, ); } List buildRouteSegments() { - if (positionHistory.length < 2) return []; + final visible = _visiblePositions; + if (visible.length < 2) return []; - final points = positionHistory + final points = visible .where(_hasValidCoords) .map((p) => LatLng(p.latitude, p.longitude)) .toList(); @@ -48,17 +87,18 @@ class RouteHistoryLayer extends StatelessWidget { if (points.length < 2) return []; return [ - Polyline(points: points, color: routeColor, strokeWidth: 4.0), + Polyline(points: points, color: routeColor, strokeWidth: 5.0), ]; } List _buildDirectionArrows() { - if (positionHistory.length < 2) return []; + final visible = _visiblePositions; + if (visible.length < 2) return []; final arrows = []; - for (int i = 0; i < positionHistory.length - 1; i++) { - final from = positionHistory[i]; - final to = positionHistory[i + 1]; + for (int i = 0; i < visible.length - 1; i++) { + final from = visible[i]; + final to = visible[i + 1]; if (!_hasValidCoords(from) || !_hasValidCoords(to)) continue; final midLat = (from.latitude + to.latitude) / 2; @@ -89,14 +129,15 @@ class RouteHistoryLayer extends StatelessWidget { } List _buildHistoryMarkers() { - if (positionHistory.isEmpty) return []; + final visible = _visiblePositions; + if (visible.isEmpty) return []; final markers = []; final threshold = _clusterBaseDegrees / math.pow(2, zoom - _clusterBaseZoom); - final first = positionHistory.first; - final last = positionHistory.last; + final first = visible.first; + final last = visible.last; markers.add( _buildEndpointMarker( @@ -105,7 +146,7 @@ class RouteHistoryLayer extends StatelessWidget { icon: Icons.play_arrow, ), ); - if (positionHistory.length > 1) { + if (visible.length > 1) { markers.add( _buildEndpointMarker( position: last, @@ -115,11 +156,11 @@ class RouteHistoryLayer extends StatelessWidget { ); } - if (positionHistory.length <= 2) return markers; + if (visible.length <= 2) return markers; - final intermediates = positionHistory.sublist( + final intermediates = visible.sublist( 1, - positionHistory.length - 1, + visible.length - 1, ); final clustered = >[]; final visited = List.filled(intermediates.length, false); @@ -214,11 +255,11 @@ class RouteHistoryLayer extends StatelessWidget { }) { return Marker( point: LatLng(position.latitude, position.longitude), - width: 18, - height: 18, + width: 28, + height: 28, child: GestureDetector( onTap: () => onPositionTap(position), - child: _CircleMarkerIcon(color: color, size: 18), + child: _CircleMarkerIcon(color: color, size: 28), ), rotate: true, );